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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest": 1,
3
3
  "package": "@cube-nl/dmp-sdk",
4
- "version": "1.1.0-rc.2",
4
+ "version": "1.1.0-rc.4",
5
5
  "file": "cube-sdk.js",
6
6
  "blocks": {
7
7
  "cube-state": {
@@ -9,6 +9,7 @@
9
9
  "since": "1.1.0",
10
10
  "summary": "A loading, empty, error, no-access or signed-out message. Every data block shows it itself; use it alone only for a custom section.",
11
11
  "actions": [],
12
+ "optional_actions": [],
12
13
  "attributes": {
13
14
  "kind": {
14
15
  "type": "enum",
@@ -46,6 +47,7 @@
46
47
  "since": "1.1.0",
47
48
  "summary": "The frame of the app: a header with logo, app name and the signed-in user with sign-in/sign-out, and, with two or more cube-page children, a page navigation that follows the address (#/<name>). Put it once around everything on the page.",
48
49
  "actions": [],
50
+ "optional_actions": [],
49
51
  "attributes": {
50
52
  "name": {
51
53
  "type": "string",
@@ -79,6 +81,7 @@
79
81
  "since": "1.1.0",
80
82
  "summary": "One page of a cube-app-shell. Holds that page's blocks; the shell shows only the active page and lists every page in its navigation.",
81
83
  "actions": [],
84
+ "optional_actions": [],
82
85
  "attributes": {
83
86
  "name": {
84
87
  "type": "id",
@@ -100,6 +103,7 @@
100
103
  "since": "1.1.0",
101
104
  "summary": "How one field of a resource is shown and edited by the cube-table, cube-detail, cube-form or cube-board it is a direct child of: its label, format, choices, or that it is left out. Never rendered itself.",
102
105
  "actions": [],
106
+ "optional_actions": [],
103
107
  "attributes": {
104
108
  "name": {
105
109
  "type": "field",
@@ -152,6 +156,7 @@
152
156
  "since": "1.1.0",
153
157
  "summary": "A period choice (this month, last 12 months, this year, or two dates) that period-aware blocks follow through their period attribute. Place it once above the blocks it drives.",
154
158
  "actions": [],
159
+ "optional_actions": [],
155
160
  "attributes": {
156
161
  "default": {
157
162
  "type": "enum",
@@ -194,6 +199,7 @@
194
199
  "actions": [
195
200
  "aggregate"
196
201
  ],
202
+ "optional_actions": [],
197
203
  "attributes": {
198
204
  "resource": {
199
205
  "type": "resource",
@@ -268,6 +274,7 @@
268
274
  "actions": [
269
275
  "aggregate"
270
276
  ],
277
+ "optional_actions": [],
271
278
  "attributes": {
272
279
  "resource": {
273
280
  "type": "resource",
@@ -374,6 +381,7 @@
374
381
  "actions": [
375
382
  "aggregate"
376
383
  ],
384
+ "optional_actions": [],
377
385
  "attributes": {
378
386
  "resource": {
379
387
  "type": "resource",
@@ -477,6 +485,7 @@
477
485
  "actions": [
478
486
  "list"
479
487
  ],
488
+ "optional_actions": [],
480
489
  "attributes": {
481
490
  "resource": {
482
491
  "type": "resource",
@@ -560,7 +569,11 @@
560
569
  "since": "1.1.0",
561
570
  "summary": "One row of a resource as a list of label and value, for the row a cube-table chose. With editable, \"Bewerken\" turns the writable fields into editors; \"Opslaan\" sends only what changed and only if nobody changed the row meanwhile, else it asks to load the newest version first.",
562
571
  "actions": [
563
- "get"
572
+ "get",
573
+ "update"
574
+ ],
575
+ "optional_actions": [
576
+ "update"
564
577
  ],
565
578
  "attributes": {
566
579
  "resource": {
@@ -569,7 +582,8 @@
569
582
  "summary": "The resource the row belongs to.",
570
583
  "example": "orders",
571
584
  "actions": [
572
- "get"
585
+ "get",
586
+ "update"
573
587
  ]
574
588
  },
575
589
  "record-id": {
@@ -609,6 +623,7 @@
609
623
  "actions": [
610
624
  "create"
611
625
  ],
626
+ "optional_actions": [],
612
627
  "attributes": {
613
628
  "resource": {
614
629
  "type": "resource",
@@ -652,6 +667,7 @@
652
667
  "since": "1.1.0",
653
668
  "summary": "A calculator that needs no data: an editor per cube-input and an output per cube-result, recalculated on every keystroke by the formula property, a JavaScript function the app sets. A formula that throws or gives NaN or Infinity shows \"—\" and one alert; the inputs keep working.",
654
669
  "actions": [],
670
+ "optional_actions": [],
655
671
  "attributes": {
656
672
  "label": {
657
673
  "type": "string",
@@ -679,6 +695,7 @@
679
695
  "since": "1.1.0",
680
696
  "summary": "One input of the cube-calc it is a direct child of. Numbers are typed the Dutch way (7,5). Never rendered itself.",
681
697
  "actions": [],
698
+ "optional_actions": [],
682
699
  "attributes": {
683
700
  "name": {
684
701
  "type": "id",
@@ -745,6 +762,7 @@
745
762
  "since": "1.1.0",
746
763
  "summary": "One labelled output of the cube-calc it is a direct child of. Never rendered itself.",
747
764
  "actions": [],
765
+ "optional_actions": [],
748
766
  "attributes": {
749
767
  "name": {
750
768
  "type": "id",
@@ -788,11 +806,14 @@
788
806
  "cube-board": {
789
807
  "kind": "block",
790
808
  "since": "1.1.0",
791
- "summary": "The rows of a resource as cards in a column per value of a status field, like a kanban board. Columns come from cube-column children, else from the enum of the status field (preview only: live, write cube-columns). A card moves by drag and drop or by a button per other column, named \"Verplaats naar …\": at once on screen, then saved; a refusal puts it back with an alert. Read-only without update or with a readonly status field: update is optional, a view-only board needs only list. Columns stack on a narrow screen.",
809
+ "summary": "The rows of a resource as cards in a column per value of a status field, like a kanban board. Columns come from cube-column children, else from the enum of the status field (preview only: live, write cube-columns). A card moves by drag and drop or by a button per other column, named \"Verplaats naar …\": at once on screen, then saved; a refusal puts it back with an alert. Read-only without update or with a readonly status field: update is optional, a view-only board needs only list. Choosing a card (click, Enter or Space) emits cube-select and can fill a cube-detail. Columns stack on a narrow screen.",
792
810
  "actions": [
793
811
  "list",
794
812
  "update"
795
813
  ],
814
+ "optional_actions": [
815
+ "update"
816
+ ],
796
817
  "attributes": {
797
818
  "resource": {
798
819
  "type": "resource",
@@ -839,6 +860,12 @@
839
860
  "required": false,
840
861
  "summary": "Cards shown per column, at most 200; a column with more says \"en nog N\".",
841
862
  "default": "50"
863
+ },
864
+ "detail": {
865
+ "type": "selector",
866
+ "required": false,
867
+ "summary": "CSS selector of a cube-detail whose record-id is set to the id of the chosen card.",
868
+ "example": "#taak"
842
869
  }
843
870
  },
844
871
  "children": [
@@ -846,7 +873,8 @@
846
873
  "cube-field"
847
874
  ],
848
875
  "events": {
849
- "cube-move": "A move was saved; detail is { resource, id, from, to } with the old and new status values. cube-data-change follows, so tables of the same resource reload."
876
+ "cube-move": "A move was saved; detail is { resource, id, from, to } with the old and new status values. cube-data-change follows, so tables of the same resource reload.",
877
+ "cube-select": "The user chose a card (click, Enter or Space; not a move); detail is { resource, id, row } with the row as listed."
850
878
  },
851
879
  "example": "<cube-board resource=\"tasks\" status-field=\"status\" title-field=\"title\" fields=\"owner\"><cube-column value=\"todo\" label=\"Te doen\"></cube-column><cube-column value=\"doing\" label=\"Bezig\"></cube-column><cube-column value=\"done\" label=\"Klaar\"></cube-column></cube-board>"
852
880
  },
@@ -855,6 +883,7 @@
855
883
  "since": "1.1.0",
856
884
  "summary": "One column of the cube-board it is a direct child of, in order. Never rendered itself.",
857
885
  "actions": [],
886
+ "optional_actions": [],
858
887
  "attributes": {
859
888
  "value": {
860
889
  "type": "string",
@@ -876,6 +905,7 @@
876
905
  "since": "1.1.0",
877
906
  "summary": "Only for a wireframe (soort \"wireframe\" in cube-app.json): one card per thing the proposal shows that the DMP does not have yet, from ontbreekt in cube-app.json: the resource or resource.field as its heading, then the screen, the kind of data and why. Without gaps (or in any other app) it says that nothing is missing. In a wireframe every block that shows invented data is marked \"Voorbeeld — bestaat nog niet\" and every write is only simulated, marked \"Voorbeeld — wordt niet opgeslagen\"; this block is not marked itself.",
878
907
  "actions": [],
908
+ "optional_actions": [],
879
909
  "attributes": {
880
910
  "scherm": {
881
911
  "type": "string",
package/dist/size.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "core_gzip": 18169,
3
- "total_gzip": 46472,
4
- "ui_gzip": 28303,
3
+ "total_gzip": 46995,
4
+ "ui_gzip": 28826,
5
5
  "budget_gzip": 40960
6
6
  }
@@ -66,7 +66,7 @@ export const BLOCK_ATTRIBUTES = {
66
66
  'cube-calc': ['label'],
67
67
  'cube-input': ['name', 'label', 'type', 'value', 'min', 'max', 'step', 'options'],
68
68
  'cube-result': ['name', 'label', 'format', 'currency', 'emphasis'],
69
- 'cube-board': ['resource', 'status-field', 'title-field', 'fields', 'sort', 'filter', 'page-size'],
69
+ 'cube-board': ['resource', 'status-field', 'title-field', 'fields', 'sort', 'filter', 'page-size', 'detail'],
70
70
  'cube-column': ['value', 'label'],
71
71
  'cube-gap-list': ['scherm'],
72
72
  };
@@ -3,6 +3,6 @@ import type { BlockScope } from '../define.js';
3
3
  /**
4
4
  * `<cube-board>`: the rows of a resource as cards in a column per value of a status field. A card moves by drag
5
5
  * and drop or by a button per other column ("Verplaats naar …"): at once on screen, then saved; a refusal puts it
6
- * back.
6
+ * back. A click on a card, or Enter or Space on it, chooses it: cube-select, and `detail` shows it.
7
7
  */
8
8
  export declare function boardBlock(Base: BlockClass, scope: BlockScope): BlockClass;
@@ -1,5 +1,6 @@
1
1
  import { isDmpError } from '../../errors.js';
2
2
  import { CARD_CSS, requiredAttr, wholeAttr } from '../aggregate-block.js';
3
+ import { AuthorError } from '../author-error.js';
3
4
  import { BlockStateError, stateFor } from '../block.js';
4
5
  import { clear, h } from '../dom.js';
5
6
  import { columnsFor, fieldList, readFieldConfigs } from '../fields.js';
@@ -14,8 +15,11 @@ position:relative;overflow-x:auto}
14
15
  section{min-width:0}
15
16
  h3{margin:0 0 .5rem;font-size:1rem}
16
17
  article{margin-top:.5rem;padding:.75rem;border:1px solid var(--cube-color-border);
17
- border-radius:var(--cube-radius-small);background:var(--cube-color-bg);overflow-wrap:anywhere}
18
+ border-radius:var(--cube-radius-small);background:var(--cube-color-bg);overflow-wrap:anywhere;
19
+ cursor:pointer}
18
20
  article[draggable=true]{cursor:grab}
21
+ article.chosen{border-color:var(--cube-color-primary);background:var(--cube-color-primary-subtle);
22
+ box-shadow:inset 4px 0 0 var(--cube-color-primary)}
19
23
  .title{margin:0;font-weight:600}
20
24
  dl{display:grid;grid-template-columns:auto 1fr;gap:0 .5rem;margin:.25rem 0 0;font-size:.875rem}
21
25
  dd{margin:0;min-width:0}
@@ -54,6 +58,10 @@ function sortOrder(order, a, b) {
54
58
  }
55
59
  return 0;
56
60
  }
61
+ /** A column value as the status field holds it: a number for a numeric field. */
62
+ function statusValue(loaded, value) {
63
+ return loaded.numeric && Number.isFinite(Number(value)) ? Number(value) : value;
64
+ }
57
65
  /** Why a move was refused, in words for the person; never the server's or the browser's own text. */
58
66
  function reason(error) {
59
67
  if (isDmpError(error) && (error.status === 409 || error.status === 412))
@@ -64,7 +72,7 @@ function reason(error) {
64
72
  /**
65
73
  * `<cube-board>`: the rows of a resource as cards in a column per value of a status field. A card moves by drag
66
74
  * and drop or by a button per other column ("Verplaats naar …"): at once on screen, then saved; a refusal puts it
67
- * back.
75
+ * back. A click on a card, or Enter or Space on it, chooses it: cube-select, and `detail` shows it.
68
76
  */
69
77
  export function boardBlock(Base, scope) {
70
78
  const doc = scope.document;
@@ -84,6 +92,9 @@ export function boardBlock(Base, scope) {
84
92
  focusKey = null;
85
93
  attempt = null;
86
94
  ids = 0;
95
+ /** The resource the choice belongs to, and the id of the chosen card. */
96
+ shownResource = null;
97
+ chosen = null;
87
98
  constructor() {
88
99
  super();
89
100
  this.shadowRoot?.insertBefore(this.alert, this.body);
@@ -110,6 +121,11 @@ export function boardBlock(Base, scope) {
110
121
  const resource = requiredAttr(this, 'resource');
111
122
  const status = requiredAttr(this, 'status-field');
112
123
  const title = requiredAttr(this, 'title-field');
124
+ if (resource !== this.shownResource) {
125
+ this.chosen = null;
126
+ this.shownResource = resource;
127
+ }
128
+ this.detailTarget();
113
129
  const filter = blockFilter(this);
114
130
  const size = Math.min(wholeAttr(this, 'page-size', 1, 50), 200);
115
131
  const sort = this.attr('sort')?.trim() ?? '';
@@ -206,11 +222,28 @@ export function boardBlock(Base, scope) {
206
222
  card(loaded, column, row, cards) {
207
223
  const key = keyOf(row);
208
224
  const title = `b${++this.ids}`;
225
+ const chosen = row.id != null && key === this.chosen;
209
226
  const card = h(doc, 'article', {
210
227
  tabindex: '0',
211
228
  draggable: loaded.canMove ? 'true' : 'false',
212
229
  'data-id': key,
213
230
  'aria-labelledby': title,
231
+ class: chosen ? 'chosen' : null,
232
+ 'aria-current': chosen ? 'true' : null,
233
+ // A move button is not a choice.
234
+ onclick: (event) => {
235
+ if (event.target.closest('button') === null)
236
+ this.select(loaded, row);
237
+ },
238
+ onkeydown: ((event) => {
239
+ if (event.target !== card || event.altKey || event.ctrlKey || event.metaKey)
240
+ return;
241
+ if (event.key !== 'Enter' && event.key !== ' ')
242
+ return;
243
+ // Space would scroll the page.
244
+ event.preventDefault();
245
+ this.select(loaded, row);
246
+ }),
214
247
  ondragstart: (event) => {
215
248
  this.dragged = key;
216
249
  event.dataTransfer?.setData('text/plain', key);
@@ -237,6 +270,43 @@ export function boardBlock(Base, scope) {
237
270
  cards.set(key, card);
238
271
  return card;
239
272
  }
273
+ /** The element `detail` names, or null without one; an invalid selector is the author's error. */
274
+ detailTarget() {
275
+ const selector = this.attr('detail')?.trim() ?? '';
276
+ if (selector === '')
277
+ return null;
278
+ try {
279
+ return doc.querySelector(selector);
280
+ }
281
+ catch {
282
+ throw new AuthorError(NL.invalid.selector(this.localName, 'detail', selector));
283
+ }
284
+ }
285
+ select(loaded, row) {
286
+ const id = row.id ?? null;
287
+ this.chosen = id === null ? null : String(id);
288
+ for (const card of Array.from(this.body.querySelectorAll('article'))) {
289
+ const chosen = card.getAttribute('data-id') === this.chosen;
290
+ card.classList.toggle('chosen', chosen);
291
+ if (chosen)
292
+ card.setAttribute('aria-current', 'true');
293
+ else
294
+ card.removeAttribute('aria-current');
295
+ }
296
+ this.emit('cube-select', { resource: loaded.resource, id, row });
297
+ if (id === null)
298
+ return;
299
+ let target = null;
300
+ try {
301
+ target = this.detailTarget();
302
+ }
303
+ catch (error) {
304
+ // Checked on load; an attribute changed since is reported the same way.
305
+ this.showState(stateFor(error));
306
+ return;
307
+ }
308
+ target?.setAttribute('record-id', String(id));
309
+ }
240
310
  /**
241
311
  * Takes the card out of its column and puts it in `to`: at its place by `sort`, else on top; null when it is
242
312
  * not on the board.
@@ -250,6 +320,8 @@ export function boardBlock(Base, scope) {
250
320
  continue;
251
321
  column.rows.splice(index, 1);
252
322
  column.total--;
323
+ // The board does not reload after its own move: the row as listed must say where it is now.
324
+ row[loaded.status] = statusValue(loaded, to);
253
325
  if (loaded.order.length === 0) {
254
326
  target.rows.unshift(row);
255
327
  }
@@ -290,7 +362,7 @@ export function boardBlock(Base, scope) {
290
362
  }
291
363
  async send(loaded, track, row, from, to) {
292
364
  const key = keyOf(row);
293
- const body = { [loaded.status]: loaded.numeric && Number.isFinite(Number(to)) ? Number(to) : to };
365
+ const body = { [loaded.status]: statusValue(loaded, to) };
294
366
  const attempt = attemptFor(this.attempt, JSON.stringify([loaded.resource, key, body]));
295
367
  this.attempt = attempt;
296
368
  try {
@@ -13,6 +13,7 @@ export interface BlockSpec {
13
13
  since: string;
14
14
  summary: string;
15
15
  actions: readonly string[];
16
+ optional_actions?: readonly string[];
16
17
  attributes: Readonly<Record<string, AttributeSpec>>;
17
18
  children?: readonly string[];
18
19
  events?: Readonly<Record<string, string>>;
@@ -24,7 +25,9 @@ export interface BlockManifest {
24
25
  package: '@cube-nl/dmp-sdk';
25
26
  version: string;
26
27
  file: 'cube-sdk.js';
27
- blocks: Record<string, BlockSpec>;
28
+ blocks: Record<string, BlockSpec & {
29
+ optional_actions: readonly string[];
30
+ }>;
28
31
  tokens: string[];
29
32
  fonts: Record<string, {
30
33
  family: string;
@@ -528,14 +528,15 @@ export const BLOCK_SPECS = {
528
528
  summary: 'One row of a resource as a list of label and value, for the row a cube-table chose. With editable, '
529
529
  + '"Bewerken" turns the writable fields into editors; "Opslaan" sends only what changed and only if '
530
530
  + 'nobody changed the row meanwhile, else it asks to load the newest version first.',
531
- actions: ['get'],
531
+ actions: ['get', 'update'],
532
+ optional_actions: ['update'],
532
533
  attributes: {
533
534
  resource: {
534
535
  type: 'resource',
535
536
  required: true,
536
537
  summary: 'The resource the row belongs to.',
537
538
  example: 'orders',
538
- actions: ['get'],
539
+ actions: ['get', 'update'],
539
540
  },
540
541
  'record-id': {
541
542
  type: 'id',
@@ -761,8 +762,10 @@ export const BLOCK_SPECS = {
761
762
  + 'live, write cube-columns). A card moves by drag and drop or by a button per other column, named '
762
763
  + '"Verplaats naar …": at once on screen, then saved; a refusal puts it back with an alert. Read-only '
763
764
  + 'without update or with a readonly status field: update is optional, a view-only board needs only list. '
765
+ + 'Choosing a card (click, Enter or Space) emits cube-select and can fill a cube-detail. '
764
766
  + 'Columns stack on a narrow screen.',
765
767
  actions: ['list', 'update'],
768
+ optional_actions: ['update'],
766
769
  attributes: {
767
770
  resource: {
768
771
  type: 'resource',
@@ -809,11 +812,19 @@ export const BLOCK_SPECS = {
809
812
  summary: 'Cards shown per column, at most 200; a column with more says "en nog N".',
810
813
  default: '50',
811
814
  },
815
+ detail: {
816
+ type: 'selector',
817
+ required: false,
818
+ summary: 'CSS selector of a cube-detail whose record-id is set to the id of the chosen card.',
819
+ example: '#taak',
820
+ },
812
821
  },
813
822
  children: ['cube-column', 'cube-field'],
814
823
  events: {
815
824
  'cube-move': 'A move was saved; detail is { resource, id, from, to } with the old and new status values. '
816
825
  + 'cube-data-change follows, so tables of the same resource reload.',
826
+ 'cube-select': 'The user chose a card (click, Enter or Space; not a move); detail is { resource, id, row } with the '
827
+ + 'row as listed.',
817
828
  },
818
829
  example: '<cube-board resource="tasks" status-field="status" title-field="title" fields="owner">'
819
830
  + '<cube-column value="todo" label="Te doen"></cube-column>'
@@ -864,13 +875,20 @@ export const BLOCK_SPECS = {
864
875
  example: '<cube-gap-list scherm="Klanten"></cube-gap-list>',
865
876
  },
866
877
  };
878
+ // Rebuilds the spec with optional_actions right after actions and every other key in its source place.
879
+ function withOptionalActions(spec) {
880
+ const entries = Object.entries(spec).filter(([key]) => key !== 'optional_actions');
881
+ const at = entries.findIndex(([key]) => key === 'actions') + 1;
882
+ entries.splice(at, 0, ['optional_actions', spec.optional_actions ?? []]);
883
+ return Object.fromEntries(entries);
884
+ }
867
885
  export function blockManifest(version) {
868
886
  return {
869
887
  manifest: 1,
870
888
  package: '@cube-nl/dmp-sdk',
871
889
  version,
872
890
  file: 'cube-sdk.js',
873
- blocks: structuredClone(BLOCK_SPECS),
891
+ blocks: Object.fromEntries(Object.entries(structuredClone(BLOCK_SPECS)).map(([tag, spec]) => [tag, withOptionalActions(spec)])),
874
892
  tokens: [...TOKEN_NAMES],
875
893
  fonts: Object.fromEntries(Object.entries(FONTS).map(([id, font]) => [
876
894
  id,
@@ -15,6 +15,8 @@ export interface TrialScope {
15
15
  /** Spec section 7: every 5 s while the page is visible, every 60 s while it is hidden. */
16
16
  export declare const POLL_VISIBLE_MS = 5000;
17
17
  export declare const POLL_HIDDEN_MS = 60000;
18
+ /** A 429's numeric Retry-After is honoured up to this long, and never past the expiry. */
19
+ export declare const RETRY_AFTER_CAP_MS = 60000;
18
20
  /** A status request that hangs this long is given up as a network error, so the polling goes on. */
19
21
  export declare const STATUS_TIMEOUT_MS = 10000;
20
22
  /**
@@ -4,6 +4,8 @@ import { NL } from './texts.js';
4
4
  /** Spec section 7: every 5 s while the page is visible, every 60 s while it is hidden. */
5
5
  export const POLL_VISIBLE_MS = 5000;
6
6
  export const POLL_HIDDEN_MS = 60000;
7
+ /** A 429's numeric Retry-After is honoured up to this long, and never past the expiry. */
8
+ export const RETRY_AFTER_CAP_MS = 60000;
7
9
  /** A status request that hangs this long is given up as a network error, so the polling goes on. */
8
10
  export const STATUS_TIMEOUT_MS = 10000;
9
11
  const CSS = `
@@ -23,9 +25,23 @@ export function showTrialBanner(scope, config) {
23
25
  const now = scope.now ?? Date.now;
24
26
  const expiresAt = Date.parse(config.expiresAt);
25
27
  const known = Number.isFinite(expiresAt);
26
- const time = known
27
- ? new Intl.DateTimeFormat('nl-NL', { hour: '2-digit', minute: '2-digit' }).format(new Date(expiresAt))
28
- : null;
28
+ const options = {
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ hourCycle: 'h23',
32
+ timeZoneName: 'short',
33
+ };
34
+ let time = null;
35
+ if (known) {
36
+ let format;
37
+ try {
38
+ format = new Intl.DateTimeFormat(doc.documentElement.lang || 'nl-NL', options);
39
+ }
40
+ catch {
41
+ format = new Intl.DateTimeFormat('nl-NL', options);
42
+ }
43
+ time = format.format(new Date(expiresAt));
44
+ }
29
45
  const host = doc.createElement('cube-trial-banner');
30
46
  const root = host.attachShadow({ mode: 'open' });
31
47
  const sheets = [BASE_CSS, CSS].map((css) => {
@@ -39,6 +55,8 @@ export function showTrialBanner(scope, config) {
39
55
  let ended = false;
40
56
  let inFlight = false;
41
57
  let abort = null;
58
+ /** Until when a 429 asked to wait; 0 when no back-off holds. */
59
+ let backOffUntil = 0;
42
60
  root.adoptedStyleSheets = sheets;
43
61
  root.appendChild(region);
44
62
  const insert = () => {
@@ -65,13 +83,14 @@ export function showTrialBanner(scope, config) {
65
83
  region.appendChild(h(doc, 'button', { type: 'button', onclick: action[1] }, action[0]));
66
84
  }
67
85
  };
68
- const schedule = () => {
86
+ const schedule = (beat = visible() ? POLL_VISIBLE_MS : POLL_HIDDEN_MS) => {
69
87
  if (ended)
70
88
  return;
71
89
  if (timer !== null)
72
90
  scope.clearTimeout(timer);
73
- const interval = visible() ? POLL_VISIBLE_MS : POLL_HIDDEN_MS;
74
- timer = scope.setTimeout(tick, known ? Math.max(0, Math.min(interval, expiresAt - now())) : interval);
91
+ // Capped again: a wall clock that jumps back must not stretch the back-off.
92
+ const wait = Math.max(beat, Math.min(RETRY_AFTER_CAP_MS, backOffUntil - now()));
93
+ timer = scope.setTimeout(tick, known ? Math.max(0, Math.min(wait, expiresAt - now())) : wait);
75
94
  };
76
95
  const tick = async () => {
77
96
  timer = null;
@@ -82,6 +101,8 @@ export function showTrialBanner(scope, config) {
82
101
  end('expired');
83
102
  return;
84
103
  }
104
+ // A back-off governs only the request it delays; the answer to this one sets the next.
105
+ backOffUntil = 0;
85
106
  inFlight = true;
86
107
  const controller = scope.AbortController === undefined ? null : new scope.AbortController();
87
108
  abort = controller;
@@ -108,6 +129,13 @@ export function showTrialBanner(scope, config) {
108
129
  const answer = async (response) => {
109
130
  if (ended)
110
131
  return;
132
+ if (response.ok)
133
+ backOffUntil = 0;
134
+ if (response.status === 429) {
135
+ const seconds = (response.headers.get('Retry-After') ?? '').trim();
136
+ if (/^\d+$/.test(seconds))
137
+ backOffUntil = now() + Math.min(RETRY_AFTER_CAP_MS, Number(seconds) * 1000);
138
+ }
111
139
  if (response.ok || response.status === 429 || response.status >= 500) {
112
140
  schedule();
113
141
  return;
@@ -151,6 +179,11 @@ export function showTrialBanner(scope, config) {
151
179
  schedule();
152
180
  return;
153
181
  }
182
+ // Back in view during a back-off: wait out its rest.
183
+ if (backOffUntil > now()) {
184
+ schedule(0);
185
+ return;
186
+ }
154
187
  // Back in view: ask right away instead of waiting out the hidden interval.
155
188
  if (timer !== null)
156
189
  scope.clearTimeout(timer);
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This package's version. A test holds it equal to package.json's. */
2
- export declare const VERSION = "1.1.0-rc.2";
2
+ export declare const VERSION = "1.1.0-rc.4";
3
3
  /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
4
  export declare const MANIFEST_SDK = "1";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This package's version. A test holds it equal to package.json's. */
2
- export const VERSION = '1.1.0-rc.2';
2
+ export const VERSION = '1.1.0-rc.4';
3
3
  /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
4
  export const MANIFEST_SDK = '1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cube-nl/dmp-sdk",
3
- "version": "1.1.0-rc.2",
3
+ "version": "1.1.0-rc.4",
4
4
  "description": "Dependency-free TypeScript client for the Cube DMP REST API (cube/api), with OAuth 2.1 + PKCE, and CubeApp for no-build apps (preview on mock data, live on the DMP).",
5
5
  "license": "MIT",
6
6
  "type": "module",