@cube-nl/dmp-sdk 1.0.0-rc.4 → 1.1.0-rc.1
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/README.md +298 -6
- package/dist/app.d.ts +36 -2
- package/dist/app.js +192 -10
- package/dist/browser.js +6 -1
- package/dist/client.d.ts +2 -0
- package/dist/cube-sdk.js +179 -2
- package/dist/cube-ui.manifest.json +971 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/preview.d.ts +9 -0
- package/dist/preview.js +39 -1
- package/dist/size.json +6 -0
- package/dist/ui/aggregate-block.d.ts +73 -0
- package/dist/ui/aggregate-block.js +138 -0
- package/dist/ui/attributes.d.ts +5 -0
- package/dist/ui/attributes.js +72 -0
- package/dist/ui/block.d.ts +63 -0
- package/dist/ui/block.js +229 -0
- package/dist/ui/blocks/app-shell.d.ts +4 -0
- package/dist/ui/blocks/app-shell.js +167 -0
- package/dist/ui/blocks/board.d.ts +8 -0
- package/dist/ui/blocks/board.js +292 -0
- package/dist/ui/blocks/calc.d.ts +9 -0
- package/dist/ui/blocks/calc.js +240 -0
- package/dist/ui/blocks/chart.d.ts +7 -0
- package/dist/ui/blocks/chart.js +166 -0
- package/dist/ui/blocks/detail.d.ts +8 -0
- package/dist/ui/blocks/detail.js +270 -0
- package/dist/ui/blocks/field.d.ts +8 -0
- package/dist/ui/blocks/field.js +16 -0
- package/dist/ui/blocks/form.d.ts +8 -0
- package/dist/ui/blocks/form.js +153 -0
- package/dist/ui/blocks/gap-list.d.ts +4 -0
- package/dist/ui/blocks/gap-list.js +36 -0
- package/dist/ui/blocks/kpi.d.ts +4 -0
- package/dist/ui/blocks/kpi.js +74 -0
- package/dist/ui/blocks/period-picker.d.ts +4 -0
- package/dist/ui/blocks/period-picker.js +155 -0
- package/dist/ui/blocks/state.d.ts +4 -0
- package/dist/ui/blocks/state.js +135 -0
- package/dist/ui/blocks/table.d.ts +8 -0
- package/dist/ui/blocks/table.js +607 -0
- package/dist/ui/blocks/top.d.ts +4 -0
- package/dist/ui/blocks/top.js +67 -0
- package/dist/ui/chart-svg.d.ts +28 -0
- package/dist/ui/chart-svg.js +111 -0
- package/dist/ui/csv.d.ts +9 -0
- package/dist/ui/csv.js +55 -0
- package/dist/ui/define.d.ts +24 -0
- package/dist/ui/define.js +107 -0
- package/dist/ui/dom.d.ts +12 -0
- package/dist/ui/dom.js +57 -0
- package/dist/ui/fields.d.ts +39 -0
- package/dist/ui/fields.js +73 -0
- package/dist/ui/format.d.ts +44 -0
- package/dist/ui/format.js +214 -0
- package/dist/ui/inputs.d.ts +35 -0
- package/dist/ui/inputs.js +161 -0
- package/dist/ui/manifest.d.ts +40 -0
- package/dist/ui/manifest.js +878 -0
- package/dist/ui/period.d.ts +18 -0
- package/dist/ui/period.js +52 -0
- package/dist/ui/query.d.ts +26 -0
- package/dist/ui/query.js +98 -0
- package/dist/ui/schema.d.ts +23 -0
- package/dist/ui/schema.js +21 -0
- package/dist/ui/styles.d.ts +8 -0
- package/dist/ui/styles.js +51 -0
- package/dist/ui/texts.d.ts +134 -0
- package/dist/ui/texts.js +122 -0
- package/dist/ui/theme.d.ts +58 -0
- package/dist/ui/theme.js +311 -0
- package/dist/ui/write.d.ts +20 -0
- package/dist/ui/write.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wireframe.d.ts +37 -0
- package/dist/wireframe.js +315 -0
- package/fonts/OFL-albert-sans.txt +93 -0
- package/fonts/OFL-ibm-plex-sans.txt +93 -0
- package/fonts/OFL-inter.txt +93 -0
- package/fonts/OFL-nunito-sans.txt +93 -0
- package/fonts/OFL-source-sans-3.txt +93 -0
- package/fonts/albert-sans.woff2 +0 -0
- package/fonts/ibm-plex-sans.woff2 +0 -0
- package/fonts/inter.woff2 +0 -0
- package/fonts/nunito-sans.woff2 +0 -0
- package/fonts/source-sans-3.woff2 +0 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ The rest of this document is in English.
|
|
|
29
29
|
- [Errors](#errors)
|
|
30
30
|
- [Voor AI-tools](#voor-ai-tools)
|
|
31
31
|
- [Route B: apps without a build step](#route-b-apps-without-a-build-step)
|
|
32
|
+
- [Bouwstenen (UI)](#bouwstenen-ui)
|
|
32
33
|
- [What this SDK deliberately does not do](#what-this-sdk-deliberately-does-not-do)
|
|
33
34
|
- [Development](#development)
|
|
34
35
|
- [Integration tests](#integration-tests)
|
|
@@ -49,9 +50,9 @@ For an app without a build step, the single-file browser build is on the npm CDN
|
|
|
49
50
|
version:
|
|
50
51
|
|
|
51
52
|
```html
|
|
52
|
-
<script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.
|
|
53
|
+
<script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
|
|
53
54
|
<!-- or -->
|
|
54
|
-
<script src="https://unpkg.com/@cube-nl/dmp-sdk@1.
|
|
55
|
+
<script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
Fallback, for an unreleased commit: `npm install
|
|
@@ -616,7 +617,7 @@ Without them the preview is permissive: every field, every function; only a fiel
|
|
|
616
617
|
|---|---|
|
|
617
618
|
| `CubeApp.connect(options?)` | `Promise<CubeAppSession>`. Options: `mock`, `manifest` (an object, or `false` to check nothing), `pageUrl` (default the page's URL), `fetch`, `tokenStorage` (`'session'`, the default, or `'memory'`), `autoSignIn` (default `false`) |
|
|
618
619
|
| `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
|
|
619
|
-
| `CubeApp.version` | This SDK's version, e.g. `'1.
|
|
620
|
+
| `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.1'` |
|
|
620
621
|
| `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
|
|
621
622
|
| `session.mode` | `'preview'` or `'live'` |
|
|
622
623
|
| `session.user()` | `{ id, name, scopes }` or `null` |
|
|
@@ -624,6 +625,11 @@ Without them the preview is permissive: every field, every function; only a fiel
|
|
|
624
625
|
| `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out |
|
|
625
626
|
| `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
|
|
626
627
|
| `session.signInError` | The error the last sign-in came back with (a refused consent: `code` `access_denied`), else `null`; `connect()` does not reject for it |
|
|
628
|
+
| `session.schema(name)` | `Promise<ResourceSchema \| null>`: one resource as a block sees it (fields, actions, what may be filtered, sorted, grouped); `null` when unknown. In a wireframe it includes the missing fields |
|
|
629
|
+
| `session.soort` | What the app is: live the config's `soort`, in the preview the manifest's (`'wireframe'`, say); `null` when neither says |
|
|
630
|
+
| `session.gaps` | A wireframe's `ontbreekt` entries (see [Wireframe](#wireframe)); `[]` for any other app |
|
|
631
|
+
| `session.isExample(resource, field?)` | Whether a wireframe shows invented data there: the whole resource, or that field of it, does not exist yet |
|
|
632
|
+
| `session.simulatesWrites` | `true` in a wireframe: writes are answered in the browser and never saved |
|
|
627
633
|
|
|
628
634
|
Live, `connect()` handles the OAuth callback itself (on the `redirectUri`), so the redirect URI can
|
|
629
635
|
be the app's own `index.html`. The tokens go to `sessionStorage` by default, unlike `createOAuth`'s
|
|
@@ -639,13 +645,299 @@ app with a build (route A).
|
|
|
639
645
|
| | |
|
|
640
646
|
|---|---|
|
|
641
647
|
| File | `dist/cube-sdk.js` in the package, `@cube-nl/dmp-sdk/cube-sdk.js` as an export; on jsDelivr/unpkg as `@cube-nl/dmp-sdk@<version>/dist/cube-sdk.js`; the CI's `pack` job also keeps it as an artifact |
|
|
642
|
-
| Size |
|
|
643
|
-
| Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.
|
|
648
|
+
| Size | 126.9 KiB minified, 42.4 KiB gzipped (17.0 KiB core + 25.5 KiB blocks) in 1.1.0-rc.1; `dist/size.json` has the exact numbers |
|
|
649
|
+
| Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.1 cube-sdk.js … */`, and `CubeApp.version` |
|
|
644
650
|
|
|
645
651
|
Pin it by copying the built file of a known version into the host, rather than fetching it at
|
|
646
652
|
runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
|
|
647
653
|
the file. Nothing from a CDN, nothing at request time.
|
|
648
654
|
|
|
655
|
+
## Bouwstenen (UI)
|
|
656
|
+
|
|
657
|
+
**Nederlands.** `cube-sdk.js` bevat ook kant-en-klare bouwstenen: custom elements met het voorvoegsel `cube-`
|
|
658
|
+
die zelf hun gegevens ophalen, de huisstijl van de klant volgen en onder de CSP van een gehost DMP-app
|
|
659
|
+
werken. De eerste set is `cube-app-shell` (kader met kop, navigatie en inloggen), `cube-page` (één pagina van
|
|
660
|
+
het kader) en `cube-state` (laden, leeg, fout, geen toegang, niet ingelogd). Alle teksten zijn Nederlands en
|
|
661
|
+
getallen, bedragen en datums zijn `nl-NL`.
|
|
662
|
+
|
|
663
|
+
```html
|
|
664
|
+
<script src="./cube-sdk.js"></script>
|
|
665
|
+
<script type="module" src="./app.js"></script>
|
|
666
|
+
|
|
667
|
+
<cube-app-shell name="Verkoop">
|
|
668
|
+
<cube-page name="overzicht" label="Overzicht">
|
|
669
|
+
<cube-state kind="empty" message="Nog geen bestellingen deze week."></cube-state>
|
|
670
|
+
</cube-page>
|
|
671
|
+
<cube-page name="details" label="Details">...</cube-page>
|
|
672
|
+
</cube-app-shell>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
How the blocks find the session: a block never signs in by itself. It takes the session the app made with
|
|
676
|
+
`CubeApp.connect()` (`CubeApp.ready`), so a block that sits in the page before the script runs still uses the app's
|
|
677
|
+
session. **Call `CubeApp.connect()` before any `await`** in your script: start it first and await it after.
|
|
678
|
+
Set the house style in the same call, `CubeApp.connect({ theme })`, or call `CubeApp.theme()` before `connect()`: a
|
|
679
|
+
`CubeApp.theme()` call made after `connect()` can be painted over by the theme the start-up applies.
|
|
680
|
+
|
|
681
|
+
```js
|
|
682
|
+
const session = CubeApp.connect(); // first, so the blocks take this session
|
|
683
|
+
const extra = await (await fetch('./extra.json')).json();
|
|
684
|
+
await session;
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
An app that never connects gets a session with the defaults, but only once the page has fully loaded (the window's
|
|
688
|
+
`load` event, plus one task), so an app that connects a little later still gets there first and the page signs in
|
|
689
|
+
once. If the app connects after that default session exists, its own session replaces it: `CubeApp.connect()`
|
|
690
|
+
dispatches `cube-session-change` on the document and every block takes the new one. That is a safety net, not a
|
|
691
|
+
way to start: in a live app the default start-up has then handled the sign-in callback with the default options.
|
|
692
|
+
A 401 shows "niet ingelogd", a 403 "geen toegang" (with the right ARIA role), never the raw message. After signing
|
|
693
|
+
in or out the shell dispatches `cube-session-change` on the document and every block loads again.
|
|
694
|
+
|
|
695
|
+
With the classic script the blocks are defined when `cube-sdk.js` loads, and `window.CubeApp` also has
|
|
696
|
+
`CubeApp.defineBlocks(options?)`. On a page without `customElements` (an old engine, a server) it does nothing and
|
|
697
|
+
`CubeApp` still works. Through npm the blocks are not defined on import: call `defineBlocks` yourself.
|
|
698
|
+
|
|
699
|
+
```js
|
|
700
|
+
import { CubeApp, defineBlocks } from '@cube-nl/dmp-sdk';
|
|
701
|
+
|
|
702
|
+
defineBlocks(); // the ESM `CubeApp` has no defineBlocks; import it from the package
|
|
703
|
+
await CubeApp.connect();
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
`defineBlocks({ session })` takes a function returning the session the blocks use; the default is `CubeApp.ready`.
|
|
707
|
+
|
|
708
|
+
The manifest `dist/cube-ui.manifest.json` (export `@cube-nl/dmp-sdk/cube-ui.manifest.json`) lists every element
|
|
709
|
+
with its attributes, events, an example and the version it appeared in (`since`), plus the 27 theme tokens and
|
|
710
|
+
the fonts. Its shape is final for 1.1. It is what Claude reads to compose an app. The build also writes `dist/size.json`
|
|
711
|
+
(`core_gzip`, `total_gzip`, `ui_gzip`, `budget_gzip`): the blocks may add at most 40 KiB gzipped to the core and the
|
|
712
|
+
build fails above it.
|
|
713
|
+
|
|
714
|
+
### Huisstijl
|
|
715
|
+
|
|
716
|
+
The house style is one file next to `index.html`: `cube-theme.json`. Its shape below is final for 1.1. The DMP writes it when an app is published;
|
|
717
|
+
without it (or with a broken one) the neutral theme applies and the app starts anyway. Read once by
|
|
718
|
+
`CubeApp.connect()`, applied to every block through `adoptedStyleSheets`.
|
|
719
|
+
|
|
720
|
+
```json
|
|
721
|
+
{
|
|
722
|
+
"theme": 1,
|
|
723
|
+
"captured_at": "2026-10-19T09:00:00Z",
|
|
724
|
+
"source": {
|
|
725
|
+
"primary_colour": "#0b5fff",
|
|
726
|
+
"accent_colour": null,
|
|
727
|
+
"font": "inter",
|
|
728
|
+
"corners": "normaal",
|
|
729
|
+
"logo": "cube-theme/logo.svg",
|
|
730
|
+
"logo_dark": null
|
|
731
|
+
},
|
|
732
|
+
"tokens": { "color-danger": "#b42318" },
|
|
733
|
+
"font_files": [{ "src": "cube-theme/inter.woff2", "weight": "100 900", "style": "normal" }]
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
| Key | Meaning |
|
|
738
|
+
|---|---|
|
|
739
|
+
| `source.primary_colour` | `#rrggbb`; everything else is derived from it. An invalid value falls back to the neutral `#475467` |
|
|
740
|
+
| `source.accent_colour` | `#rrggbb` or `null` (then the hue of the primary colour turned 330 degrees) |
|
|
741
|
+
| `source.font` | `system`, `inter`, `albert-sans`, `source-sans-3`, `nunito-sans` or `ibm-plex-sans` |
|
|
742
|
+
| `source.corners` | `scherp` (0 px), `normaal` (6 px) or `rond` (12 px) |
|
|
743
|
+
| `source.logo`, `source.logo_dark` | `cube-theme/logo.<ext>` next to the app, or `null` |
|
|
744
|
+
| `tokens` | Optional per-token overrides; only a valid value is used, anything else keeps the derived one |
|
|
745
|
+
| `font_files` | Optional; default the files of the chosen font as `cube-theme/<file>` |
|
|
746
|
+
|
|
747
|
+
The tokens are the CSS custom properties `--cube-<name>` on every block: `color-primary`, `color-primary-hover`,
|
|
748
|
+
`color-primary-subtle`, `color-primary-text`, `color-on-primary`, `color-accent`, `color-on-accent`, `color-text`,
|
|
749
|
+
`color-text-muted`, `color-bg`, `color-surface`, `color-border`, `color-focus`, `color-danger`, `color-success`,
|
|
750
|
+
`color-warning`, `color-example-bg`, `color-example-text`, `color-series-1` to `color-series-6`, `radius`,
|
|
751
|
+
`radius-small` and `font-family`. Text colours are checked for WCAG 2.1 AA contrast: `color-on-primary` and
|
|
752
|
+
`color-on-accent` on their colours, and `color-primary-text` (also the focus ring) at 4.5:1 on both the surface
|
|
753
|
+
`#f6f8fa` and `color-primary-subtle`; a primary colour that falls short is darkened for text in 5% steps (warning
|
|
754
|
+
`primary-darkened-for-text`).
|
|
755
|
+
|
|
756
|
+
A value that could break out of the stylesheet never reaches it: colours must be `#rrggbb`, radii a number of `px`
|
|
757
|
+
or `rem`, and `font-family` a comma-separated list of names (quoted names allowed, but no unbalanced quote, no
|
|
758
|
+
semicolon, brace, backslash or comment). Logo and font addresses are kept only when they are on the page's own
|
|
759
|
+
origin (or a base64 `data:` image) and hold no quote, parenthesis or whitespace. A `data:` logo shows in the chat
|
|
760
|
+
preview, but a hosted app's CSP (`img-src 'self'`) blocks it: a published app uses `cube-theme/logo.<ext>`.
|
|
761
|
+
|
|
762
|
+
The theme sheet sets the tokens on `:where(:root)`, which has no specificity, so an app's own stylesheet can
|
|
763
|
+
override any token in `:root`, for example `:root { --cube-color-primary: #0b5fff; }`.
|
|
764
|
+
|
|
765
|
+
`CubeApp.theme(fileOrSource)` registers a theme from code (a `cube-theme.json` object, or just its `source`) and
|
|
766
|
+
applies it at once; `CubeApp.theme(null)` forgets it. `connect({ theme })` takes the same value, or `false` for
|
|
767
|
+
the neutral theme without reading `cube-theme.json`. The font files ship in the package under `fonts/`; a host
|
|
768
|
+
serves them as `cube-theme/<file>` (for example `fonts/inter.woff2` as `cube-theme/inter.woff2`). Each font has
|
|
769
|
+
its own OFL licence text next to it.
|
|
770
|
+
|
|
771
|
+
### Blokken
|
|
772
|
+
|
|
773
|
+
Every block is a custom element; the full list with attributes lives in the manifest. Row data is only
|
|
774
|
+
ever rendered as text, never as HTML. Every control works with the keyboard and shows a focus ring, and the
|
|
775
|
+
blocks work from 320 px wide.
|
|
776
|
+
|
|
777
|
+
| Element | Use |
|
|
778
|
+
|---|---|
|
|
779
|
+
| `cube-app-shell` | `name` (required), `logo`, `logo-alt`. Header, user with Inloggen/Uitloggen, page navigation that follows `#/<name>` |
|
|
780
|
+
| `cube-page` | `name` (id in the address), `label` (text in the navigation). A configuration element: it only holds blocks |
|
|
781
|
+
| `cube-state` | `kind`: `loading`, `empty`, `error`, `forbidden`, `signed-out`; `message` replaces the standard text; `retry="false"` leaves out the retry button. Event `cube-retry` |
|
|
782
|
+
| `cube-period-picker` | `default` (`this-month`, `12m`, `this-year`, `custom`), `label`, `today`. Feeds other blocks; `value` is `{ key, from, to, previous, today }`, event `cube-change` |
|
|
783
|
+
| `cube-kpi` | `resource`, `measure` (required), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `compare`. One figure; `compare` adds the change against the previous period (`nieuw` when that was zero) |
|
|
784
|
+
| `cube-chart` | `resource`, `measure`, `by` (required), `kind` (`line`/`bar`), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`, `height`, `limit`. Own SVG, no chart library; a table for screen readers |
|
|
785
|
+
| `cube-top` | `resource`, `measure`, `by` (required), `limit`, `order`, `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`. Sorted and cut in the browser (an aggregate cannot be sorted by the API). Over 1000 groups the DMP refuses the aggregate (`422 cardinality_exceeded`, reason `too_many_groups`); the block then asks for a shorter period or a filter, without a retry. Event `cube-select` |
|
|
786
|
+
| `cube-field` | `name` (required), `label`, `format` (`text`, `number`, `integer`, `currency`, `percent`, `date`, `datetime`, `boolean`), `multiline`, `options` (`value=Label,...`), `hidden`. A configuration element, a direct child of `cube-table`, `cube-detail`, `cube-form` or `cube-board`; it is never rendered. Live `_capabilities` carries no `format` or `enum`, so a date column or a choice filter there comes from `format`/`options` here or from the values themselves, never from a guess at the type |
|
|
787
|
+
| `cube-table` | `resource` (required), `fields`, `search` (one field, box "Zoeken"), `filters` (a control per field: choices, Alle/Ja/Nee, from-to dates or numbers, else contains), `sort` (`-` for descending), `page-size` (default 25, at most 200), `export`, `detail` (selector of a `cube-detail`), `filter`, `period`, `date-field`. Needs `list`. Search waits 300 ms and the last answer wins. Enter or Space chooses a row, which stays marked (`aria-current`). Event `cube-select` |
|
|
788
|
+
| `cube-detail` | `resource` (required), `record-id`, `fields`, `editable`. Needs `get`, and `update` for `editable`. Events `cube-saved`, `cube-data-change` |
|
|
789
|
+
| `cube-form` | `resource` (required), `fields`, `submit-label` (default "Opslaan"), `success-text` (default "Opgeslagen. Dank je wel!"). Needs `create`. Events `cube-created`, `cube-data-change` |
|
|
790
|
+
| `cube-calc` | `label`. A calculator that needs no data: an editor per `cube-input`, an output per `cube-result`, recalculated on every keystroke by the `formula` property. Read-only `values` and `results`, event `cube-change` (`{ values, results }`) |
|
|
791
|
+
| `cube-input` | `name`, `label` (required), `type` (`number`, `integer`, `currency`, `percent`, `text`, `select`, `checkbox`), `value`, `min`, `max`, `step`, `options` (a select needs it; `value=Label,...` as in `cube-field`). A configuration element, a direct child of `cube-calc`; it is never rendered. Numbers are typed the Dutch way (`7,5`) |
|
|
792
|
+
| `cube-result` | `name`, `label` (required), `format` (`number`, `integer`, `currency`, `percent`, `text`), `currency` (default `EUR`), `emphasis` (the large figure). A configuration element, a direct child of `cube-calc`; it is never rendered |
|
|
793
|
+
| `cube-board` | `resource`, `status-field`, `title-field` (required), `fields` (extra lines on a card), `sort`, `filter`, `page-size` (default 50, at most 200; a column with more says "en nog N"). `cube-field` children label and format the `fields` of a card. Cards in a column per status value. `status-field` must be filterable and `sort` a sortable field. Needs `list`; `update` is optional (without it the board is read-only). Event `cube-move` (`{ resource, id, from, to }`), followed by `cube-data-change` |
|
|
794
|
+
| `cube-column` | `value` (required), `label`. A configuration element, a direct child of `cube-board`, in order; it is never rendered |
|
|
795
|
+
|
|
796
|
+
`cube-table`, `cube-detail` and `cube-form` need the matching actions in the app's `uses`
|
|
797
|
+
(`"orders": ["list", "get", "create", "update"]`). A table sends the row it chose to the `cube-detail` named by
|
|
798
|
+
`detail="#bestelling"`; after a save or a create every block on that resource loads again (`cube-data-change`).
|
|
799
|
+
|
|
800
|
+
- **CSV export** (`export`): the filtered rows, at most 10 000, with `;` as the separator, a UTF-8 BOM and CRLF line
|
|
801
|
+
ends, so a Dutch spreadsheet opens it with the right columns. A text value that starts with `=`, `+`, `-`, `@`, a
|
|
802
|
+
tab or a carriage return gets a `'` in front (formula injection); a genuine number in a numeric column, such as
|
|
803
|
+
`-5` or `12,5`, is not prefixed (in a text column `-5` becomes `'-5`). Pressing the button again while it runs
|
|
804
|
+
cancels.
|
|
805
|
+
- **Filters**: `filter` and the followed period are always applied; the controls can only narrow them. A bound
|
|
806
|
+
takes the stricter of the two (a "Vanaf" before the period start keeps the period start), and a field that
|
|
807
|
+
`filter` fixes to one value gets no control. A `filters` or `search` field the resource cannot be filtered on
|
|
808
|
+
(not in the schema's `filterable`) shows an error naming that field instead of a table.
|
|
809
|
+
- **Required fields**: a field the DMP requires shows " (verplicht)" after its label, besides `aria-required`.
|
|
810
|
+
- **Writes**: every submit attempt carries an `Idempotency-Key` (`newIdempotencyKey()`), the button is disabled
|
|
811
|
+
while saving, and a retry after a network error reuses the key, so a double click creates one row. `cube-detail`
|
|
812
|
+
sends only the changed fields with `ifMatch` set to the `recordVersion` it loaded. When someone else changed the
|
|
813
|
+
row meanwhile (409 or 412) it shows "Iemand anders heeft deze rij intussen gewijzigd" with "Opnieuw laden", keeps
|
|
814
|
+
what you typed and does not retry by itself. Read-only fields are never shown as an input and never sent.
|
|
815
|
+
- **Numbers**: a number field takes Dutch input. `12,5` is sent as `12.5`, `1.234` is one thousand two hundred
|
|
816
|
+
thirty-four (the dot is a thousands separator), `12.5` also works, and `abc` is refused in the browser with
|
|
817
|
+
"Vul een getal in."
|
|
818
|
+
|
|
819
|
+
```html
|
|
820
|
+
<cube-table resource="orders" fields="reference,placed_on,total,status" search="reference" filters="status,placed_on"
|
|
821
|
+
export detail="#bestelling">
|
|
822
|
+
<cube-field name="total" label="Bedrag" format="currency"></cube-field>
|
|
823
|
+
</cube-table>
|
|
824
|
+
<cube-detail id="bestelling" resource="orders" editable></cube-detail>
|
|
825
|
+
<cube-form resource="orders" fields="reference,placed_on,total,status"></cube-form>
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
The three data blocks (`cube-kpi`, `cube-chart`, `cube-top`) read aggregates, so the app's `cube-app.json` needs
|
|
829
|
+
`aggregate` in `uses` for each resource they read; `lookup` also needs `list` on the lookup resource
|
|
830
|
+
(`"orders": ["list", "aggregate"], "customers": ["list"]`). The `cube-period-picker` reads nothing itself: it only
|
|
831
|
+
feeds them. `period="#periode"` points at a picker by CSS selector and needs `date-field`, the date the period
|
|
832
|
+
filters on, except on a `cube-chart` whose `by` is a truncated date (`placed_on.month`): that field is used.
|
|
833
|
+
`compare` needs `period`. The period filter is always `{ gte: from, lt: dayAfterTo }` on that field, never
|
|
834
|
+
`between`, so it is right for dates and datetimes alike. A chart leaves the buckets after the picker's `today` empty
|
|
835
|
+
(a gap, not a zero). An aggregate over more than 1000 groups is refused by the DMP; `cube-chart` and `cube-top` then
|
|
836
|
+
say so ("Te veel groepen…") and ask for a shorter period or a filter, without a retry button.
|
|
837
|
+
|
|
838
|
+
```html
|
|
839
|
+
<cube-period-picker id="periode" default="12m"></cube-period-picker>
|
|
840
|
+
<cube-kpi resource="orders" measure="sum(total)" label="Omzet" format="currency"
|
|
841
|
+
period="#periode" date-field="placed_on" compare></cube-kpi>
|
|
842
|
+
<cube-chart resource="orders" measure="sum(total)" by="placed_on.month" period="#periode"></cube-chart>
|
|
843
|
+
<cube-top resource="orders" measure="sum(total)" by="customer_id" lookup="customers.name"
|
|
844
|
+
period="#periode" date-field="placed_on"></cube-top>
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
#### Calculator and board
|
|
848
|
+
|
|
849
|
+
- **Formula** (`cube-calc`): `formula` is a synchronous JavaScript function property, set from a script, never an
|
|
850
|
+
attribute (an `async` one returns a Promise, which shows "—"). A string is refused and nothing is evaluated, so it
|
|
851
|
+
works under the CSP. It gets `values` by `cube-input` name and returns an object by `cube-result` name (or one
|
|
852
|
+
number or text when there is a single result):
|
|
853
|
+
|
|
854
|
+
```js
|
|
855
|
+
document.querySelector('cube-calc').formula = ({ uren, tarief, btw }) => ({
|
|
856
|
+
totaal: uren * tarief * (1 + btw),
|
|
857
|
+
});
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
A `percent` input is typed as `21` and arrives as the fraction `0.21`; an empty or invalid input arrives as
|
|
861
|
+
`null` (also below `min`, above `max` or off `step`). A result that is `null` or `undefined` just shows "—". A
|
|
862
|
+
formula that throws, or a result that is `NaN` or `Infinity`, shows "—" and one alert; the inputs keep working. Two
|
|
863
|
+
or more `cube-result`s need an object back: a single number or text then shows an error instead of the calculator.
|
|
864
|
+
`cube-change` follows a recalculation by typing or by a new formula on a drawn calculator; not the first draw.
|
|
865
|
+
- **Columns** (`cube-board`): the columns come from the `cube-column` children, in that order. Without them the
|
|
866
|
+
board takes the `enum` of the status field, but live `_capabilities` carries no `enum`, so a starter must always
|
|
867
|
+
write the `cube-column`s (live without any shows an error). Each column loads its own cards, filtered on the
|
|
868
|
+
status field, so that field must be filterable on the resource (and `sort` a sortable field), else the board
|
|
869
|
+
shows an error; a status value without a column is not shown.
|
|
870
|
+
- **Moving**: drag and drop, or a compact button per other column on each card, so it works with the keyboard too.
|
|
871
|
+
A button shows an arrow and the column name ("→ Bezig"); its accessible name is "Verplaats naar Bezig". The card
|
|
872
|
+
moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert.
|
|
873
|
+
- **Read-only**: without `update` in the app's `uses`, or when the status field is read-only, cards cannot move and
|
|
874
|
+
show no buttons. Columns stack on a narrow screen (from 320 px).
|
|
875
|
+
|
|
876
|
+
### Wireframe
|
|
877
|
+
|
|
878
|
+
A wireframe is a proposal that shows what an app would look like, partly on real data and partly on invented data.
|
|
879
|
+
The kind is `soort`: `"wireframe"` in `cube-app.json`, copied by the DMP into `cube-app.config.json`. Only a
|
|
880
|
+
wireframe gets any of what follows; a live app without `soort: "wireframe"` never reads `cube-mock.json` and never
|
|
881
|
+
answers from invented data, and a broken server stays an error.
|
|
882
|
+
|
|
883
|
+
What the DMP does not have yet is listed in `ontbreekt` in `cube-app.json`, one entry per missing field or per
|
|
884
|
+
missing resource:
|
|
885
|
+
|
|
886
|
+
```json
|
|
887
|
+
{
|
|
888
|
+
"soort": "wireframe",
|
|
889
|
+
"ontbreekt": [
|
|
890
|
+
{ "resource": "orders", "veld": "delivery_note", "type": "tekst", "scherm": "Lijst",
|
|
891
|
+
"reden": "Een opmerking voor de bezorger staat nog niet bij een bestelling." },
|
|
892
|
+
{ "resource": "visits", "type": "tekst", "scherm": "Wat ontbreekt",
|
|
893
|
+
"reden": "Bezoeken aan de winkel worden nog nergens vastgelegd." }
|
|
894
|
+
]
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
`resource` is a REST resource name, `veld` the missing field (left out when the whole resource is missing), `type`
|
|
899
|
+
one of `tekst`, `getal`, `bedrag`, `datum`, `keuze`, `ja/nee`, `waarden` the choices (required for `keuze`), `scherm`
|
|
900
|
+
the screen that shows it and `reden` why it is needed. The SDK only reads this; `cube/apps` validates it. A
|
|
901
|
+
malformed entry is dropped with one `console.warn`.
|
|
902
|
+
|
|
903
|
+
- **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the
|
|
904
|
+
row's `id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`.
|
|
905
|
+
The live request never names a missing field: a filter or sort on it applies to the returned page only, and an
|
|
906
|
+
aggregate that groups by or sums it is answered from the mock rows. The preview does the same; there, an aggregate
|
|
907
|
+
on a missing field needs that field in the rows of `cube-mock.json`.
|
|
908
|
+
- **Marking.** Blocks mark what is invented: `Voorbeeld — bestaat nog niet`. A table or detail marks the column or
|
|
909
|
+
field; the other blocks mark the whole block.
|
|
910
|
+
- **Simulated writes.** `create`, `update`, `delete`, `operation` and `collectionOperation` are answered in the
|
|
911
|
+
browser, in live and in preview, and forgotten on reload. A block that writes says
|
|
912
|
+
`Voorbeeld — wordt niet opgeslagen`. A wireframe never writes real data: that covers everything that goes through
|
|
913
|
+
the session (`session.resource()` and the blocks). App code that builds its own client with the exported
|
|
914
|
+
`createClient` bypasses it.
|
|
915
|
+
- **Numbers.** KPI and chart numbers do not reflect simulated writes: they come from the rows the session holds
|
|
916
|
+
without them.
|
|
917
|
+
- **`cube-gap-list`.** One card per gap, for the people who build the real thing: what is missing
|
|
918
|
+
(`resource.veld`) as its heading, then Scherm, Soort gegeven and Waarom. It stays readable at 320 px.
|
|
919
|
+
`scherm="Lijst"` shows only that screen's gaps; without gaps it says nothing is missing.
|
|
920
|
+
|
|
921
|
+
```html
|
|
922
|
+
<cube-page name="wat-ontbreekt" label="Wat ontbreekt">
|
|
923
|
+
<cube-gap-list></cube-gap-list>
|
|
924
|
+
</cube-page>
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
`npm run visual` serves the pages with the hosted-app CSP; add `?wireframe=1` to see a wireframe with two gaps.
|
|
928
|
+
|
|
929
|
+
### CSP
|
|
930
|
+
|
|
931
|
+
The blocks run under `default-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; script-src 'self'`,
|
|
932
|
+
the policy of a hosted app. Therefore: no inline `<script>` or `<style>`, no `style` attribute, no `innerHTML`;
|
|
933
|
+
styles are made with `CSSStyleSheet.replaceSync` and `adoptedStyleSheets`. The test suite guards this (a grep of
|
|
934
|
+
the bundle and a walk through the rendered blocks). Your own page must follow the same rule: load `cube-sdk.js`
|
|
935
|
+
and your module with `src`, and keep the logo, the fonts and the mock data on the same origin.
|
|
936
|
+
|
|
937
|
+
To look at the blocks under that policy, run `npm run visual` and open `http://127.0.0.1:4173/`,
|
|
938
|
+
`/?huisstijl=b` and `/?huisstijl=geen`. The server (`scripts/serve-visual.mjs`, bound to 127.0.0.1) sends the
|
|
939
|
+
policy above on every response and serves only `test/visual/`, `dist/cube-sdk.js`, the test mock and the fonts.
|
|
940
|
+
|
|
649
941
|
## What this SDK deliberately does not do
|
|
650
942
|
|
|
651
943
|
- **No per-resource generated types.** `_capabilities` is per token and at runtime, and OpenAPI
|
|
@@ -679,7 +971,7 @@ dev dependencies; there are no runtime dependencies, and there should not be.
|
|
|
679
971
|
### Releasing
|
|
680
972
|
|
|
681
973
|
Bump `version` in `package.json` and `src/version.ts` (a test holds them equal), add the CHANGELOG
|
|
682
|
-
entry, merge, then push a tag that equals the version (e.g. `1.
|
|
974
|
+
entry, merge, then push a tag that equals the version (e.g. `1.1.0-rc.1`). The tag pipeline's
|
|
683
975
|
`publish` job checks that, builds, and runs `npm publish`: a version with a `-` goes to the `next`
|
|
684
976
|
dist-tag, any other to `latest`. It needs the masked, protected CI variable `NPM_TOKEN` (an npm
|
|
685
977
|
granular access token with write access to the `cube-nl` org).
|
package/dist/app.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { type AuthorizationServerMetadata } from './auth.js';
|
|
|
2
2
|
import { type ResourceClient } from './client.js';
|
|
3
3
|
import { type AppManifest, type MockData } from './preview.js';
|
|
4
4
|
import type { DmpRecord } from './types.js';
|
|
5
|
+
import { type ResourceSchema } from './ui/schema.js';
|
|
6
|
+
import { type ThemeFile, type ThemeSource } from './ui/theme.js';
|
|
7
|
+
import { type GapEntry } from './wireframe.js';
|
|
5
8
|
/**
|
|
6
9
|
* `cube-app.config.json`, which the DMP writes next to the app only when it publishes an approved
|
|
7
10
|
* release. Never part of what the maker or an AI sends: its presence is what makes an app live.
|
|
@@ -21,6 +24,8 @@ export interface CubeAppConfig {
|
|
|
21
24
|
issuer?: string;
|
|
22
25
|
/** Passed to `createOAuth`: endpoints to use instead of (or on top of) discovery. */
|
|
23
26
|
metadata?: Partial<AuthorizationServerMetadata>;
|
|
27
|
+
/** `'wireframe'`: what exists runs live, what `ontbreekt` in cube-app.json lists comes from `cube-mock.json`. */
|
|
28
|
+
soort?: string;
|
|
24
29
|
}
|
|
25
30
|
export interface ConnectOptions {
|
|
26
31
|
/** Preview data: `cube.mock_data`'s output. Default: what `CubeApp.mock()` registered, else `./cube-mock.json`. */
|
|
@@ -40,6 +45,11 @@ export interface ConnectOptions {
|
|
|
40
45
|
tokenStorage?: 'session' | 'memory';
|
|
41
46
|
/** Live only: go to the DMP's sign-in right away when nobody is signed in. Default false. */
|
|
42
47
|
autoSignIn?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The house style. Default: what `CubeApp.theme()` registered, else `./cube-theme.json`, else the
|
|
50
|
+
* neutral theme. `false`: neutral, and `./cube-theme.json` is not read.
|
|
51
|
+
*/
|
|
52
|
+
theme?: ThemeFile | ThemeSource | false;
|
|
43
53
|
}
|
|
44
54
|
export interface CubeAppUser {
|
|
45
55
|
/** The DMP's user id (the token's `sub`), or `'preview'`. Null when the token does not say. */
|
|
@@ -62,6 +72,16 @@ export interface CubeAppSession {
|
|
|
62
72
|
resource<T extends DmpRecord = DmpRecord>(name: string): ResourceClient<T>;
|
|
63
73
|
/** Why the last sign-in came back without a session (consent refused, say); null otherwise. */
|
|
64
74
|
readonly signInError: Error | null;
|
|
75
|
+
/** One resource as a block sees it: fields, actions, what may be filtered, sorted, grouped. Null when unknown. */
|
|
76
|
+
schema(resource: string): Promise<ResourceSchema | null>;
|
|
77
|
+
/** What the app is: live the config's `soort`, in the preview the manifest's. Null when neither says. */
|
|
78
|
+
readonly soort: string | null;
|
|
79
|
+
/** A wireframe's gap entries: what it shows that the DMP does not have yet. Empty for any other app. */
|
|
80
|
+
readonly gaps: readonly GapEntry[];
|
|
81
|
+
/** Whether a wireframe shows invented data here: the whole resource, or this field of it, does not exist yet. */
|
|
82
|
+
isExample(resource: string, field?: string): boolean;
|
|
83
|
+
/** A wireframe: writes are answered in the browser and never saved. */
|
|
84
|
+
readonly simulatesWrites: boolean;
|
|
65
85
|
}
|
|
66
86
|
/**
|
|
67
87
|
* Starts the app. Reads `./cube-app.config.json` next to the page: absent, the app runs in the
|
|
@@ -69,17 +89,31 @@ export interface CubeAppSession {
|
|
|
69
89
|
* OAuth (authorization code + PKCE) and talks to the DMP's REST face. The app's code is the same.
|
|
70
90
|
*/
|
|
71
91
|
declare function connect(options?: ConnectOptions): Promise<CubeAppSession>;
|
|
92
|
+
/**
|
|
93
|
+
* The session `connect()` made for this page, or one with the defaults when the app never connected. The default one
|
|
94
|
+
* starts only once the page has loaded (and one task later), so an app that awaits something before it calls
|
|
95
|
+
* `connect()` still gets there first and the page signs in once.
|
|
96
|
+
*/
|
|
97
|
+
declare function ready(): Promise<CubeAppSession>;
|
|
72
98
|
/**
|
|
73
99
|
* Registers preview data before `connect()`, for an app that embeds its mock data in a script tag
|
|
74
100
|
* instead of shipping `cube-mock.json`. Has no effect on a live app. `null` forgets it again.
|
|
75
101
|
*/
|
|
76
102
|
declare function mock(data: MockData | null): void;
|
|
103
|
+
/**
|
|
104
|
+
* Registers the house style before `connect()`, for an app that holds it in code instead of shipping
|
|
105
|
+
* `cube-theme.json`, and applies it right away. `null` forgets it again (neutral).
|
|
106
|
+
*/
|
|
107
|
+
declare function theme(input: ThemeFile | ThemeSource | null): void;
|
|
77
108
|
/** What `cube-sdk.js` puts on `window`, and what this package exports as `CubeApp`. */
|
|
78
109
|
export declare const CubeApp: Readonly<{
|
|
79
110
|
connect: typeof connect;
|
|
111
|
+
/** The session the app connected, or one connected with the defaults. What blocks use. */
|
|
112
|
+
ready: typeof ready;
|
|
80
113
|
mock: typeof mock;
|
|
81
|
-
|
|
82
|
-
version
|
|
114
|
+
theme: typeof theme;
|
|
115
|
+
/** This SDK's version, e.g. `'1.1.0-rc.1'`. */
|
|
116
|
+
version: "1.1.0-rc.1";
|
|
83
117
|
/** The manifest `"sdk"` value this build serves. */
|
|
84
118
|
sdk: "1";
|
|
85
119
|
}>;
|