@cube-nl/dmp-sdk 1.1.0-rc.1 → 1.1.0-rc.3
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 +115 -27
- package/dist/app.d.ts +7 -4
- package/dist/app.js +89 -33
- package/dist/browser.js +8 -0
- package/dist/client.js +5 -1
- package/dist/cube-sdk.js +32 -22
- package/dist/cube-ui.manifest.json +12 -5
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +21 -6
- package/dist/size.json +3 -3
- package/dist/trial.d.ts +32 -0
- package/dist/trial.js +55 -0
- package/dist/ui/aggregate-block.js +2 -1
- package/dist/ui/attributes.js +1 -1
- package/dist/ui/author-error.d.ts +3 -0
- package/dist/ui/author-error.js +3 -0
- package/dist/ui/block.d.ts +8 -2
- package/dist/ui/block.js +14 -5
- package/dist/ui/blocks/app-shell.js +1 -1
- package/dist/ui/blocks/board.d.ts +1 -1
- package/dist/ui/blocks/board.js +129 -16
- package/dist/ui/blocks/calc.js +35 -12
- package/dist/ui/blocks/kpi.js +2 -1
- package/dist/ui/blocks/table.js +82 -23
- package/dist/ui/dom.js +2 -1
- package/dist/ui/format.js +3 -2
- package/dist/ui/manifest.js +14 -3
- package/dist/ui/query.js +7 -6
- package/dist/ui/texts.d.ts +12 -0
- package/dist/ui/texts.js +16 -0
- package/dist/ui/trial-banner.d.ts +27 -0
- package/dist/ui/trial-banner.js +199 -0
- package/dist/ui/write.d.ts +2 -2
- package/dist/ui/write.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wireframe.js +53 -16
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -50,9 +50,9 @@ For an app without a build step, the single-file browser build is on the npm CDN
|
|
|
50
50
|
version:
|
|
51
51
|
|
|
52
52
|
```html
|
|
53
|
-
<script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.
|
|
53
|
+
<script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.3/dist/cube-sdk.js"></script>
|
|
54
54
|
<!-- or -->
|
|
55
|
-
<script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.
|
|
55
|
+
<script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.3/dist/cube-sdk.js"></script>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Fallback, for an unreleased commit: `npm install
|
|
@@ -578,6 +578,76 @@ What the preview cannot do: mock data names no relations, so an `include` or `wi
|
|
|
578
578
|
in `meta.omitted` (show it as "not visible", as live) and `has` is not applied; custom operations
|
|
579
579
|
are `404 not_found`; `location` and `etag` are null.
|
|
580
580
|
|
|
581
|
+
### Trial (proefversie)
|
|
582
|
+
|
|
583
|
+
A maker can try an app on their own data before it is approved: the DMP serves the app's files in a sandboxed page
|
|
584
|
+
(CSP `sandbox`, an opaque origin) and writes a read-only token into that page. The SDK then runs the app as a third
|
|
585
|
+
mode, `'trial'`, with the same code as preview and live.
|
|
586
|
+
|
|
587
|
+
The DMP inserts one data block as the first child of `<head>` (a JSON data block, not a script, so the CSP does not
|
|
588
|
+
apply to it):
|
|
589
|
+
|
|
590
|
+
```html
|
|
591
|
+
<script type="application/json" id="cube-trial-config">{"mode":"trial","dmpUrl":"https://dmp.example.nl",
|
|
592
|
+
"apiUrl":"https://dmp.example.nl/apps/api/v1","token":"cubeproef_…","expiresAt":"2026-10-20T15:42:00+02:00",
|
|
593
|
+
"statusUrl":"https://dmp.example.nl/apps/api/v1/_trial","version":"1.0.3","soort":null,"userId":"42",
|
|
594
|
+
"leest":["orders","klanten"]}</script>
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
| Member | |
|
|
598
|
+
|---|---|
|
|
599
|
+
| `mode` | Must be `"trial"` |
|
|
600
|
+
| `dmpUrl`, `apiUrl`, `token`, `expiresAt`, `statusUrl` | Required non-empty strings. `expiresAt` is ISO 8601 |
|
|
601
|
+
| `version`, `soort`, `userId` | A string or `null` |
|
|
602
|
+
| `leest` | The resources the trial reads, for the banner; default `[]` |
|
|
603
|
+
|
|
604
|
+
- **Read once, then removed.** When `cube-sdk.js` (or the ES module, in a browser) evaluates, it reads the element,
|
|
605
|
+
removes it from the DOM and keeps the config in memory. A config that is not JSON, not `mode: "trial"` or misses a
|
|
606
|
+
member gives one `console.warn` (without its content) and the app starts as it would without it.
|
|
607
|
+
- **The session.** `connect()` checks the trial config first: it does not fetch `cube-app.config.json`, does no OAuth
|
|
608
|
+
(no discovery, no PKCE, no callback) and touches neither `sessionStorage` nor `localStorage`. Every request carries
|
|
609
|
+
`Authorization: Bearer cubeproef_…` to `apiUrl`. `user()` is `{ id: userId, name: null, scopes: ['api:read'] }`;
|
|
610
|
+
`signIn()` reloads the page (the DMP writes a token on every load); `signOut()` makes calls from the app stop carrying
|
|
611
|
+
the token (they answer `401`); the banner's status poll keeps it. The SDK refuses a write in a trial before it
|
|
612
|
+
leaves the page (the browser would block it at the CORS preflight): it fails as the DMP's `403` would, a `DmpError`
|
|
613
|
+
with status 403 that blocks show as "no access", never faked; only a
|
|
614
|
+
wireframe (`soort: "wireframe"`) runs in the same mixed mode as live, with simulated writes.
|
|
615
|
+
- **The token is never stored and never logged.** It lives in a variable of the page for as long as the page. This
|
|
616
|
+
keeps it out of storage, logs and an innocent `innerHTML` dump; it is no defence against hostile code in the page,
|
|
617
|
+
which the DMP bounds instead (read-only, bound to the maker's IP and `Origin: null`, at most 60 minutes).
|
|
618
|
+
- **The banner.** `cube-sdk.js` puts `<cube-trial-banner>` first in `<body>` (shadow DOM, constructable stylesheet,
|
|
619
|
+
house-style tokens): *"Proefversie · alleen jij · alleen lezen · verloopt om 15:42 CEST · leest: orders, klanten ·
|
|
620
|
+
Plak geen gegevens uit deze proefversie in je AI-gesprek."*
|
|
621
|
+
- **Which clock.** The time shown is the DMP's expiry moment (`expiresAt` from the page's config), formatted in nl-NL
|
|
622
|
+
in the browser's time zone with that zone's short name (a browser in UTC shows "13:42 UTC"). A page in the DMP,
|
|
623
|
+
such as the app page, shows the time in the DMP's time zone; the zone name keeps the two from seeming to disagree.
|
|
624
|
+
The banner ends as expired without a request when the browser's clock passes `expiresAt`; a `401 trial_expired`
|
|
625
|
+
from the DMP ends it whatever the browser's clock says. The DMP's refusal is what ends the trial: a browser clock
|
|
626
|
+
that runs behind only shows the expired state later, and one that runs ahead shows "verlopen" early while the DMP
|
|
627
|
+
would still answer.
|
|
628
|
+
- **The poll.** It asks `GET statusUrl` (with the token, `cache: 'no-store'`, a 10 s timeout) every 5 s while the
|
|
629
|
+
page is visible and every 60 s while hidden, and right away when the page becomes visible again. When `expiresAt`
|
|
630
|
+
passes it shows the expired state without a request. A `429` with a `Retry-After` in whole seconds (digits only)
|
|
631
|
+
makes the next request wait that long, at least the normal interval and at most 60 s (`RETRY_AFTER_CAP_MS`), and
|
|
632
|
+
never past `expiresAt`; while that back-off holds, becoming visible again waits out its rest instead of asking at
|
|
633
|
+
once; it covers only the request it delays. Another `Retry-After` (absent, an HTTP date) keeps the normal rhythm.
|
|
634
|
+
The page can only read `Retry-After` when the DMP exposes it (`cube/apps` does by default for trial requests, via
|
|
635
|
+
`Access-Control-Expose-Headers`); without it the normal rhythm holds.
|
|
636
|
+
|
|
637
|
+
| Answer | Banner |
|
|
638
|
+
|---|---|
|
|
639
|
+
| `200` | Nothing changes, polling goes on |
|
|
640
|
+
| Network error, timeout, `5xx` | Polling goes on |
|
|
641
|
+
| `429` | Polling goes on, after a numeric `Retry-After` (capped at 60 s) when it is longer than the interval |
|
|
642
|
+
| `401 trial_replaced`, `wider: false` | *"Nieuwe versie wordt geladen…"*, then `location.assign(newer)` |
|
|
643
|
+
| `401 trial_replaced`, otherwise | *"Er is een nieuwe versie die meer gegevens leest."* with **Laden**: only the maker's click navigates |
|
|
644
|
+
| `401 trial_elsewhere` | *"Laad de pagina opnieuw."* with **Opnieuw laden** |
|
|
645
|
+
| `401 trial_expired` | *"Deze proefversie is verlopen. Vraag in je gesprek om een nieuwe."* |
|
|
646
|
+
| `401 trial_closed`, any other answer | *"Deze proefversie is gesloten."* |
|
|
647
|
+
|
|
648
|
+
`newer` is followed only when it has `dmpUrl`'s origin and a path under `/apps/proef/`; any other URL counts as
|
|
649
|
+
closed. Every state except `200` and the transient ones stops the polling.
|
|
650
|
+
|
|
581
651
|
### Mock data
|
|
582
652
|
|
|
583
653
|
The shape is `cube.mock_data`'s output as is: per REST resource the `dmp.describe` payload and the
|
|
@@ -617,12 +687,12 @@ Without them the preview is permissive: every field, every function; only a fiel
|
|
|
617
687
|
|---|---|
|
|
618
688
|
| `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`) |
|
|
619
689
|
| `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
|
|
620
|
-
| `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.
|
|
690
|
+
| `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.3'` |
|
|
621
691
|
| `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
|
|
622
|
-
| `session.mode` | `'preview'` or `'
|
|
692
|
+
| `session.mode` | `'preview'`, `'live'` or `'trial'` (a proefversie the DMP serves: the maker's own data, read-only, with a token written into the page) |
|
|
623
693
|
| `session.user()` | `{ id, name, scopes }` or `null` |
|
|
624
|
-
| `session.signIn()` | Live: navigates to the DMP's sign-in. Preview: signs the stand-in in |
|
|
625
|
-
| `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out |
|
|
694
|
+
| `session.signIn()` | Live: navigates to the DMP's sign-in. Preview: signs the stand-in in. Trial: reloads the page |
|
|
695
|
+
| `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out. Trial: forgets the token, calls then answer 401 |
|
|
626
696
|
| `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
|
|
627
697
|
| `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
698
|
| `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 |
|
|
@@ -645,8 +715,8 @@ app with a build (route A).
|
|
|
645
715
|
| | |
|
|
646
716
|
|---|---|
|
|
647
717
|
| 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 |
|
|
648
|
-
| Size |
|
|
649
|
-
| Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.
|
|
718
|
+
| Size | 136.2 KiB minified, 45.9 KiB gzipped (17.7 KiB core + 28.1 KiB blocks and the trial banner) in 1.1.0-rc.3; `dist/size.json` has the exact numbers |
|
|
719
|
+
| Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.3 cube-sdk.js … */`, and `CubeApp.version` |
|
|
650
720
|
|
|
651
721
|
Pin it by copying the built file of a known version into the host, rather than fetching it at
|
|
652
722
|
runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
|
|
@@ -778,19 +848,19 @@ blocks work from 320 px wide.
|
|
|
778
848
|
|---|---|
|
|
779
849
|
| `cube-app-shell` | `name` (required), `logo`, `logo-alt`. Header, user with Inloggen/Uitloggen, page navigation that follows `#/<name>` |
|
|
780
850
|
| `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` |
|
|
851
|
+
| `cube-state` | `kind`: `loading`, `empty`, `error`, `forbidden`, `signed-out`; `message` replaces the standard text; `retry="false"` leaves out the retry button. Event `cube-retry`. A block's error state shows the Dutch reason for a wrong attribute; any other failure shows the general text, and a network error or the SDK's own `TypeError` is also logged in the console |
|
|
782
852
|
| `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
853
|
| `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
854
|
| `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
855
|
| `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
856
|
| `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
|
|
857
|
+
| `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. The rows are one Tab stop: arrow keys, Home and End move between rows; Enter or Space chooses one, which stays marked (`aria-current`). Event `cube-select` |
|
|
788
858
|
| `cube-detail` | `resource` (required), `record-id`, `fields`, `editable`. Needs `get`, and `update` for `editable`. Events `cube-saved`, `cube-data-change` |
|
|
789
859
|
| `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
860
|
| `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
861
|
| `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
862
|
| `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` |
|
|
863
|
+
| `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"), `detail` (selector of a `cube-detail`). `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`; event `cube-select` (`{ resource, id, row }`) when a card is chosen |
|
|
794
864
|
| `cube-column` | `value` (required), `label`. A configuration element, a direct child of `cube-board`, in order; it is never rendered |
|
|
795
865
|
|
|
796
866
|
`cube-table`, `cube-detail` and `cube-form` need the matching actions in the app's `uses`
|
|
@@ -804,7 +874,9 @@ blocks work from 320 px wide.
|
|
|
804
874
|
cancels.
|
|
805
875
|
- **Filters**: `filter` and the followed period are always applied; the controls can only narrow them. A bound
|
|
806
876
|
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 `
|
|
877
|
+
`filter` fixes to one value gets no control. A `search` box or a contains filter on a field that `filter` fixes or
|
|
878
|
+
gives an `ilike`, or a choice filter on a field that `filter` limits with an operator (such as `in`), changes
|
|
879
|
+
nothing (the base wins); the console warns once. A `filters` or `search` field the resource cannot be filtered on
|
|
808
880
|
(not in the schema's `filterable`) shows an error naming that field instead of a table.
|
|
809
881
|
- **Required fields**: a field the DMP requires shows " (verplicht)" after its label, besides `aria-required`.
|
|
810
882
|
- **Writes**: every submit attempt carries an `Idempotency-Key` (`newIdempotencyKey()`), the button is disabled
|
|
@@ -847,7 +919,7 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
|
|
|
847
919
|
#### Calculator and board
|
|
848
920
|
|
|
849
921
|
- **Formula** (`cube-calc`): `formula` is a synchronous JavaScript function property, set from a script, never an
|
|
850
|
-
attribute (an `async` one
|
|
922
|
+
attribute (an `async` one is refused with an error). A string is refused and nothing is evaluated, so it
|
|
851
923
|
works under the CSP. It gets `values` by `cube-input` name and returns an object by `cube-result` name (or one
|
|
852
924
|
number or text when there is a single result):
|
|
853
925
|
|
|
@@ -859,8 +931,12 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
|
|
|
859
931
|
|
|
860
932
|
A `percent` input is typed as `21` and arrives as the fraction `0.21`; an empty or invalid input arrives as
|
|
861
933
|
`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
|
|
863
|
-
|
|
934
|
+
formula that throws, or a result that is `NaN`, `Infinity` or an object (for a number, integer, currency or percent
|
|
935
|
+
result), shows "—" and one alert that stays in place; the inputs keep working. A formula that throws logs
|
|
936
|
+
one `console.warn` per failure streak. Two or more `cube-result`s need an object back: a single number or text then
|
|
937
|
+
shows an error instead of the calculator on draw, but while typing it is a failed calculation ("—" and the alert),
|
|
938
|
+
as the formula may just have taken another branch. The `name` of each `cube-input`, and of each `cube-result`,
|
|
939
|
+
must be unique: a duplicate is refused.
|
|
864
940
|
`cube-change` follows a recalculation by typing or by a new formula on a drawn calculator; not the first draw.
|
|
865
941
|
- **Columns** (`cube-board`): the columns come from the `cube-column` children, in that order. Without them the
|
|
866
942
|
board takes the `enum` of the status field, but live `_capabilities` carries no `enum`, so a starter must always
|
|
@@ -869,9 +945,17 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
|
|
|
869
945
|
shows an error; a status value without a column is not shown.
|
|
870
946
|
- **Moving**: drag and drop, or a compact button per other column on each card, so it works with the keyboard too.
|
|
871
947
|
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
|
-
|
|
874
|
-
|
|
948
|
+
moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert. The card
|
|
949
|
+
is placed in the new column by `sort` until the next load, which shows the server's order (on top without
|
|
950
|
+
`sort`). A status field of type integer or number
|
|
951
|
+
gets the column value as a number.
|
|
952
|
+
- **Read-only**: without `update` in the app's `uses`, when the status field is read-only, or when the resource has no
|
|
953
|
+
`id` field (the board then does not request `id`), cards cannot move and show no buttons.
|
|
954
|
+
- **Choosing**: a click on a card, or Enter or Space while the card itself has focus, chooses it: `cube-select` with
|
|
955
|
+
`{ resource, id, row }` (as `cube-table`), and `detail` sets the `record-id` of that `cube-detail`. A move (a
|
|
956
|
+
button or a drop) is never a choice. The chosen card stays marked (`aria-current`) across a move and a reload until
|
|
957
|
+
another `resource` is shown; a card without `id` emits `id: null`, is not marked and fills no detail.
|
|
958
|
+
Columns stack on a narrow screen (from 320 px).
|
|
875
959
|
|
|
876
960
|
### Wireframe
|
|
877
961
|
|
|
@@ -898,19 +982,23 @@ missing resource:
|
|
|
898
982
|
`resource` is a REST resource name, `veld` the missing field (left out when the whole resource is missing), `type`
|
|
899
983
|
one of `tekst`, `getal`, `bedrag`, `datum`, `keuze`, `ja/nee`, `waarden` the choices (required for `keuze`), `scherm`
|
|
900
984
|
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
|
-
|
|
905
|
-
|
|
906
|
-
aggregate that groups by or sums it is answered from the mock rows. The preview
|
|
907
|
-
on a missing field needs that field in the rows of `cube-mock.json`.
|
|
985
|
+
malformed entry, or an `ontbreekt` that is not a list, is dropped with one `console.warn`.
|
|
986
|
+
|
|
987
|
+
- **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the row's
|
|
988
|
+
`id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`. The live
|
|
989
|
+
request never names a missing field: a filter or sort on it applies to the returned page only (the page's count then
|
|
990
|
+
counts only the rows it shows), and an aggregate that groups by or sums it is answered from the mock rows. The preview
|
|
991
|
+
does the same; there, an aggregate on a missing field needs that field in the rows of `cube-mock.json`. A gap of type
|
|
992
|
+
`bedrag` shows as money also when the mock data has the field; other types follow the field's values.
|
|
908
993
|
- **Marking.** Blocks mark what is invented: `Voorbeeld — bestaat nog niet`. A table or detail marks the column or
|
|
909
994
|
field; the other blocks mark the whole block.
|
|
910
995
|
- **Simulated writes.** `create`, `update`, `delete`, `operation` and `collectionOperation` are answered in the
|
|
911
996
|
browser, in live and in preview, and forgotten on reload. A block that writes says
|
|
912
|
-
`Voorbeeld — wordt niet opgeslagen`. A
|
|
913
|
-
the
|
|
997
|
+
`Voorbeeld — wordt niet opgeslagen`. A sorted list places created rows, rows that join it and rows changed in a
|
|
998
|
+
sort field into the server's order when its rows carry the sort field; without a sort, created rows come first on
|
|
999
|
+
page 1. They also work signed out, and the preview does not check them against `uses`: a wireframe shows the idea,
|
|
1000
|
+
not the access rules. A wireframe never writes real data: that covers everything that
|
|
1001
|
+
goes through the session (`session.resource()` and the blocks). App code that builds its own client with the exported
|
|
914
1002
|
`createClient` bypasses it.
|
|
915
1003
|
- **Numbers.** KPI and chart numbers do not reflect simulated writes: they come from the rows the session holds
|
|
916
1004
|
without them.
|
package/dist/app.d.ts
CHANGED
|
@@ -60,8 +60,11 @@ export interface CubeAppUser {
|
|
|
60
60
|
scopes: string[];
|
|
61
61
|
}
|
|
62
62
|
export interface CubeAppSession {
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* `'preview'`: mock data in this browser, no DMP. `'live'`: the real DMP, as the signed-in person. `'trial'`: the
|
|
65
|
+
* maker's own data, read-only, with the token the DMP wrote into the page.
|
|
66
|
+
*/
|
|
67
|
+
readonly mode: 'preview' | 'live' | 'trial';
|
|
65
68
|
/** Who is signed in, or null. In the preview a stand-in user is signed in from the start. */
|
|
66
69
|
user(): CubeAppUser | null;
|
|
67
70
|
/** Live: navigates to the DMP's sign-in (the page is gone after this). Preview: signs the stand-in in again. */
|
|
@@ -112,8 +115,8 @@ export declare const CubeApp: Readonly<{
|
|
|
112
115
|
ready: typeof ready;
|
|
113
116
|
mock: typeof mock;
|
|
114
117
|
theme: typeof theme;
|
|
115
|
-
/** This SDK's version, e.g. `'1.1.0-rc.
|
|
116
|
-
version: "1.1.0-rc.
|
|
118
|
+
/** This SDK's version, e.g. `'1.1.0-rc.3'`. */
|
|
119
|
+
version: "1.1.0-rc.3";
|
|
117
120
|
/** The manifest `"sdk"` value this build serves. */
|
|
118
121
|
sdk: "1";
|
|
119
122
|
}>;
|
package/dist/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PreviewStore } from './preview.js';
|
|
|
5
5
|
import { schemaFromCapability } from './ui/schema.js';
|
|
6
6
|
import { PREVIEW_USER_NAME } from './ui/texts.js';
|
|
7
7
|
import { resolveTheme, setTheme } from './ui/theme.js';
|
|
8
|
+
import { trialConfig } from './trial.js';
|
|
8
9
|
import { MANIFEST_SDK, VERSION } from './version.js';
|
|
9
10
|
import { mixedResource, readGaps, simulatedWrites, withGaps } from './wireframe.js';
|
|
10
11
|
const CONFIG_FILE = './cube-app.config.json';
|
|
@@ -91,13 +92,18 @@ function announceSessionChange() {
|
|
|
91
92
|
async function start(options, isCurrent) {
|
|
92
93
|
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
93
94
|
const pageUrl = options.pageUrl ?? currentPage();
|
|
95
|
+
// A trial page never has cube-app.config.json and never signs in with OAuth: the DMP wrote the token into it.
|
|
96
|
+
const trial = trialConfig();
|
|
94
97
|
const [config, theme] = await Promise.all([
|
|
95
|
-
readJson(doFetch, resolve(CONFIG_FILE, pageUrl)),
|
|
98
|
+
trial === null ? readJson(doFetch, resolve(CONFIG_FILE, pageUrl)) : null,
|
|
96
99
|
loadTheme(options, doFetch, pageUrl),
|
|
97
100
|
]);
|
|
98
101
|
if (isCurrent()) {
|
|
99
102
|
setTheme(theme);
|
|
100
103
|
}
|
|
104
|
+
if (trial !== null) {
|
|
105
|
+
return trialSession(trial, options, doFetch, pageUrl);
|
|
106
|
+
}
|
|
101
107
|
if (config === null) {
|
|
102
108
|
return preview(options, doFetch, pageUrl);
|
|
103
109
|
}
|
|
@@ -198,6 +204,47 @@ async function preview(options, doFetch, pageUrl) {
|
|
|
198
204
|
schema: async (name) => withGaps(store(name).describe(name), marked.gaps),
|
|
199
205
|
};
|
|
200
206
|
}
|
|
207
|
+
/** A wireframe's marks and its mock data. Only a wireframe reads the mock: an ordinary app never fetches it. */
|
|
208
|
+
async function wireframeParts(soort, options, doFetch, pageUrl) {
|
|
209
|
+
const [mock, manifest] = soort === 'wireframe' ? await appFiles(options, doFetch, pageUrl) : [{}, null];
|
|
210
|
+
// The manifest's ceiling is the live client's: a missing resource is not in it, so the store checks none.
|
|
211
|
+
return { marked: marks(soort, manifest), examples: new PreviewStore(mock) };
|
|
212
|
+
}
|
|
213
|
+
/** The session on the DMP, live or trial: a wireframe mixes in its gaps, everything else goes to the client. */
|
|
214
|
+
function onDmp(connection, { marked, examples }) {
|
|
215
|
+
const { client } = connection;
|
|
216
|
+
let capabilities = null;
|
|
217
|
+
return {
|
|
218
|
+
...marked,
|
|
219
|
+
mode: connection.mode,
|
|
220
|
+
get signInError() {
|
|
221
|
+
return connection.signInError();
|
|
222
|
+
},
|
|
223
|
+
user: connection.user,
|
|
224
|
+
signIn: connection.signIn,
|
|
225
|
+
signOut: connection.signOut,
|
|
226
|
+
resource: writes(marked.simulatesWrites, (name) => marked.isExample(name)
|
|
227
|
+
? examples.resource(name)
|
|
228
|
+
: marked.gaps.some((gap) => gap.resource === name)
|
|
229
|
+
? mixedResource(client.resources(name), marked.gaps, examples, examples.rows(name))
|
|
230
|
+
: client.resources(name)),
|
|
231
|
+
async schema(name) {
|
|
232
|
+
if (marked.isExample(name)) {
|
|
233
|
+
return withGaps(examples.describe(name), marked.gaps);
|
|
234
|
+
}
|
|
235
|
+
capabilities ??= client.capabilities();
|
|
236
|
+
try {
|
|
237
|
+
const found = (await capabilities).resources.find((resource) => resource.name === name);
|
|
238
|
+
return found === undefined ? null : withGaps(schemaFromCapability(found), marked.gaps);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// A failed call (signed out, offline) must not stay cached for the whole session.
|
|
242
|
+
capabilities = null;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
201
248
|
async function live(config, options, doFetch, pageUrl) {
|
|
202
249
|
const dmpUrl = config.dmpUrl.replace(/\/+$/, '');
|
|
203
250
|
let tokens = null;
|
|
@@ -215,12 +262,7 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
215
262
|
});
|
|
216
263
|
const client = createClient({ baseUrl: config.apiUrl ?? `${dmpUrl}/apps/api/v1`, auth: oauth, fetch: doFetch });
|
|
217
264
|
let signInError = null;
|
|
218
|
-
|
|
219
|
-
// Only a wireframe reads the mock data: an ordinary live app never fetches cube-mock.json, even when it ships one.
|
|
220
|
-
const [mock, manifest] = config.soort === 'wireframe' ? await appFiles(options, doFetch, pageUrl) : [{}, null];
|
|
221
|
-
const marked = marks(config.soort, manifest);
|
|
222
|
-
// The manifest's ceiling is the live client's: a missing resource is not in it, so the store checks none.
|
|
223
|
-
const examples = new PreviewStore(mock);
|
|
265
|
+
const parts = await wireframeParts(config.soort, options, doFetch, pageUrl);
|
|
224
266
|
tokens = await store.get();
|
|
225
267
|
try {
|
|
226
268
|
await oauth.handleCallback();
|
|
@@ -232,12 +274,10 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
232
274
|
if (tokens === null && signInError === null && options.autoSignIn === true) {
|
|
233
275
|
await oauth.signIn();
|
|
234
276
|
}
|
|
235
|
-
return {
|
|
236
|
-
...marked,
|
|
277
|
+
return onDmp({
|
|
237
278
|
mode: 'live',
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
},
|
|
279
|
+
client,
|
|
280
|
+
signInError: () => signInError,
|
|
241
281
|
user: () => (tokens === null ? null : userFrom(tokens)),
|
|
242
282
|
async signIn() {
|
|
243
283
|
signInError = null;
|
|
@@ -246,28 +286,44 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
246
286
|
async signOut() {
|
|
247
287
|
await oauth.signOut();
|
|
248
288
|
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
capabilities = null;
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
},
|
|
289
|
+
}, parts);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* A trial reads only. A write never leaves the page (the browser would block it at the CORS preflight and the app would
|
|
293
|
+
* see a TypeError): it answers the 403 the DMP would, so it surfaces as a DmpError like any refusal.
|
|
294
|
+
*/
|
|
295
|
+
function readOnly(doFetch) {
|
|
296
|
+
return async (input, init) => {
|
|
297
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
298
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
299
|
+
return doFetch(input, init);
|
|
300
|
+
}
|
|
301
|
+
return new Response(JSON.stringify({ status: 403, code: 'insufficient_scope' }), {
|
|
302
|
+
status: 403,
|
|
303
|
+
headers: { 'Content-Type': 'application/problem+json' },
|
|
304
|
+
});
|
|
269
305
|
};
|
|
270
306
|
}
|
|
307
|
+
/** The live session with the injected token instead of OAuth: no discovery, no callback, no storage. */
|
|
308
|
+
async function trialSession(config, options, doFetch, pageUrl) {
|
|
309
|
+
let token = config.token;
|
|
310
|
+
// Read on every request: once signed out no token goes out, so the DMP answers 401.
|
|
311
|
+
const client = createClient({ baseUrl: config.apiUrl, auth: () => token, fetch: readOnly(doFetch) });
|
|
312
|
+
const parts = await wireframeParts(config.soort ?? undefined, options, doFetch, pageUrl);
|
|
313
|
+
return onDmp({
|
|
314
|
+
mode: 'trial',
|
|
315
|
+
client,
|
|
316
|
+
signInError: () => null,
|
|
317
|
+
user: () => (token === null ? null : { id: config.userId, name: null, scopes: ['api:read'] }),
|
|
318
|
+
// The DMP writes a token into the page on every load: a reload is the sign-in.
|
|
319
|
+
async signIn() {
|
|
320
|
+
globalThis.location?.reload();
|
|
321
|
+
},
|
|
322
|
+
async signOut() {
|
|
323
|
+
token = null;
|
|
324
|
+
},
|
|
325
|
+
}, parts);
|
|
326
|
+
}
|
|
271
327
|
/**
|
|
272
328
|
* Who the token belongs to, read from the access token itself (cube/oauth-server issues JWTs).
|
|
273
329
|
* For display only: the DMP checks the token on every request, this SDK does not verify it.
|
|
@@ -420,7 +476,7 @@ export const CubeApp = Object.freeze({
|
|
|
420
476
|
ready,
|
|
421
477
|
mock,
|
|
422
478
|
theme,
|
|
423
|
-
/** This SDK's version, e.g. `'1.1.0-rc.
|
|
479
|
+
/** This SDK's version, e.g. `'1.1.0-rc.3'`. */
|
|
424
480
|
version: VERSION,
|
|
425
481
|
/** The manifest `"sdk"` value this build serves. */
|
|
426
482
|
sdk: MANIFEST_SDK,
|
package/dist/browser.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { CubeApp as Core } from './app.js';
|
|
2
|
+
import { trialConfig } from './trial.js';
|
|
2
3
|
import { defineBlocks } from './ui/define.js';
|
|
4
|
+
import { showTrialBanner } from './ui/trial-banner.js';
|
|
3
5
|
// The entry of dist/cube-sdk.js: one classic script, no imports, `window.CubeApp` for a page without a build step.
|
|
4
6
|
const CubeApp = Object.freeze({ ...Core, defineBlocks });
|
|
5
7
|
globalThis.CubeApp = CubeApp;
|
|
6
8
|
// An engine without custom elements (an old browser, a script run outside a page) keeps the plain CubeApp.
|
|
7
9
|
if (typeof customElements !== 'undefined')
|
|
8
10
|
defineBlocks();
|
|
11
|
+
// A trial page (the config was read when trial.ts evaluated): say so at the top, and follow the trial's status.
|
|
12
|
+
const trial = trialConfig();
|
|
13
|
+
const page = globalThis;
|
|
14
|
+
if (trial !== null && page.document !== undefined && page.CSSStyleSheet !== undefined) {
|
|
15
|
+
showTrialBanner(page, trial);
|
|
16
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -99,7 +99,11 @@ export function createClient(options) {
|
|
|
99
99
|
body: input ?? {},
|
|
100
100
|
headers: writeHeaders(opts),
|
|
101
101
|
});
|
|
102
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
...response,
|
|
104
|
+
data: response.status === 204 ? null : response.data,
|
|
105
|
+
accepted: response.status === 202,
|
|
106
|
+
};
|
|
103
107
|
};
|
|
104
108
|
return {
|
|
105
109
|
name,
|