@disy/cadenza.js 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cadenza.js CHANGED
@@ -4,23 +4,28 @@
4
4
  * @param {string} baseUrl - The base URL of the Cadenza server
5
5
  * @param {object} [options] - Options
6
6
  * @param {HTMLIFrameElement | string} [options.iframe] - An iframe for embedding Cadenza or the iframe's ID
7
+ * @param {ExternalLinkKey} [options.webApplication] - An external link that Cadenza uses to resolve the [target origin](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#targetorigin) when posting events. This is required if Cadenza and your application are not running on the same origin.
7
8
  * @param {boolean} [options.debug] - Whether to enable debug logging
8
- * @throws For invalid base URL
9
+ * @throws For invalid arguments
9
10
  */
10
11
  export function cadenza(baseUrl, options) {
11
12
  return new CadenzaClient(baseUrl, options);
12
13
  }
13
14
  /* @ts-ignore */
14
15
  const previousGlobalCadenza = globalThis.cadenza;
15
- globalThis.cadenza = Object.assign(
16
- /** @param {Parameters<cadenza>} args */
17
- (...args) => cadenza(...args), {
16
+ globalThis.cadenza = Object.assign((/** @type Parameters<cadenza> */ ...args) => cadenza(...args), {
18
17
  noConflict() {
19
18
  globalThis.cadenza = previousGlobalCadenza;
20
19
  return cadenza;
21
20
  },
22
21
  });
23
22
  /** @typedef {string} EmbeddingTargetId - The ID of an embedding target */
23
+ /** @typedef {string} GlobalId - The ID of a navigator item */
24
+ /**
25
+ * @typedef ExternalLinkKey - A tuple qualifying a Cadenza external link
26
+ * @property {string} repositoryName - The name of the link's repository
27
+ * @property {string} externalLinkId - The ID of the external link
28
+ */
24
29
  /**
25
30
  * @typedef WorkbookKey - A tuple qualifying a workbook
26
31
  * @property {string} repositoryName - The name of the workbook's repository
@@ -41,6 +46,14 @@ globalThis.cadenza = Object.assign(
41
46
  /** @typedef {EmbeddingTargetId | WorkbookKey} WorkbookSource - A workbook source */
42
47
  /** @typedef {EmbeddingTargetId | WorksheetKey} WorksheetSource - A worksheet source */
43
48
  /** @typedef {EmbeddingTargetId | WorkbookViewKey} WorkbookViewSource - A workbook view source */
49
+ /** @typedef {'normal'|'simplified'} OperationMode - The mode in which a workbook should be operated */
50
+ /**
51
+ * @typedef {'workbook-design'|'workbook-view-management'} UiFeature - The name of a Cadenza UI feature
52
+ *
53
+ * _Note:_ Supported features are:
54
+ * * 'workbook-design' - The workbook designer
55
+ * * 'workbook-view-management' - Add/Edit/Remove workbook views (Is included in 'workbook-design'.)
56
+ * */
44
57
  /**
45
58
  * @typedef Geometry - A [GeoJSON](https://geojson.org/) geometry object
46
59
  * @property {GeometryType} type - The type of the geometry
@@ -67,6 +80,8 @@ export class CadenzaClient {
67
80
  #origin;
68
81
  /** @readonly */
69
82
  #iframe;
83
+ /** @readonly @type {ExternalLinkKey | undefined} */
84
+ #webApplication;
70
85
  /** @type {HTMLIFrameElement | undefined} */
71
86
  #iframeElement;
72
87
  /** @readonly */
@@ -78,19 +93,24 @@ export class CadenzaClient {
78
93
  * @param {string} baseUrl
79
94
  * @param {object} [options]
80
95
  * @param {HTMLIFrameElement | string} [options.iframe]
96
+ * @param {ExternalLinkKey} [options.webApplication]
81
97
  * @param {boolean} [options.debug]
82
98
  */
83
- constructor(baseUrl, { debug = false, iframe } = {}) {
99
+ constructor(baseUrl, { debug = false, iframe, webApplication } = {}) {
84
100
  assert(validUrl(baseUrl), `Invalid baseUrl: ${baseUrl}`);
101
+ if (webApplication) {
102
+ assert(validExternalLinkKey(webApplication), `Invalid webApplication parameter: ${webApplication}`);
103
+ }
85
104
  // Remove trailing /
86
105
  if (baseUrl.at(-1) === '/') {
87
106
  baseUrl = baseUrl.substring(0, baseUrl.length - 1);
88
107
  }
89
- this.#log('Create Cadenza client', baseUrl, iframe);
90
108
  this.#baseUrl = baseUrl;
91
109
  this.#origin = new URL(baseUrl).origin;
92
110
  this.#iframe = iframe;
93
111
  this.#debug = debug;
112
+ this.#webApplication = webApplication;
113
+ this.#log('Create Cadenza client', baseUrl, iframe);
94
114
  }
95
115
  /** The base URL of the Cadenza server this client is requesting */
96
116
  get baseUrl() {
@@ -117,48 +137,69 @@ export class CadenzaClient {
117
137
  *
118
138
  * @param {WorkbookSource | WorksheetSource | WorkbookViewSource} source - The source to show
119
139
  * @param {object} [options]
120
- * @param {boolean} [options.hideMainHeaderAndFooter] - Whether to hide the main Cadenza header and footer.
121
- * @param {boolean} [options.hideWorkbookToolBar] - Whether to hide the workbook toolbar.
140
+ * @param {UiFeature[]} [options.disabledUiFeatures] - Cadenza UI features to disable
141
+ * @param {boolean} [options.hideMainHeaderAndFooter] - Whether to hide the main Cadenza header and footer
142
+ * @param {boolean} [options.hideWorkbookToolBar] - Whether to hide the workbook toolbar
143
+ * @param {GlobalId} [options.highlightGlobalId] - The ID of an item to highlight / expand in the navigator
144
+ * @param {string} [options.mediaType] - Set to 'application/pdf' for views of type "JasperReports report"
145
+ * to show the report PDF directly, without any Cadenza headers or footers.
146
+ * @param {OperationMode} [options.operationMode] - The mode in which a workbook should be operated
122
147
  * @param {AbortSignal} [options.signal] - A signal to abort the iframe loading
123
148
  * @return {Promise<void>} A Promise for when the iframe is loaded
124
- * @throws For an invalid source
149
+ * @throws For invalid arguments
125
150
  */
126
- show(source, { hideMainHeaderAndFooter, hideWorkbookToolBar, signal } = {}) {
151
+ show(source, { disabledUiFeatures, hideMainHeaderAndFooter, hideWorkbookToolBar, highlightGlobalId, mediaType, operationMode, signal, } = {}) {
127
152
  this.#log('CadenzaClient#show', source);
128
- const params = new URLSearchParams({
129
- ...(hideMainHeaderAndFooter && { hideMainHeaderAndFooter: 'true' }),
130
- ...(hideWorkbookToolBar && { hideWorkbookToolBar: 'true' }),
153
+ if (mediaType) {
154
+ assertSupportedMediaType(mediaType, [MediaType.PDF]);
155
+ }
156
+ const params = createParams({
157
+ disabledUiFeatures,
158
+ hideMainHeaderAndFooter,
159
+ hideWorkbookToolBar,
160
+ highlightGlobalId,
161
+ operationMode,
162
+ mediaType,
163
+ webApplication: this.#webApplication,
131
164
  });
132
- return this.#show(resolvePath(source), { params, signal });
165
+ return this.#show(resolvePath(source), params, signal);
133
166
  }
134
167
  /**
135
168
  * Show a workbook map view in an iframe.
136
169
  *
137
170
  * @param {WorkbookViewSource} mapView - The workbook map view to show
138
171
  * @param {object} [options] - Options
172
+ * @param {UiFeature[]} [options.disabledUiFeatures] - Cadenza UI features to disable
139
173
  * @param {Geometry} [options.geometry] - A geometry to show on the map
140
- * @param {boolean} [options.hideMainHeaderAndFooter] - Whether to hide the main Cadenza header and footer.
141
- * @param {boolean} [options.hideWorkbookToolBar] - Whether to hide the workbook toolbar.
174
+ * @param {boolean} [options.hideMainHeaderAndFooter] - Whether to hide the main Cadenza header and footer
175
+ * @param {boolean} [options.hideWorkbookToolBar] - Whether to hide the workbook toolbar
176
+ * @param {GlobalId} [options.highlightGlobalId] - The ID of an item to highlight / expand in the navigator
142
177
  * @param {string} [options.locationFinder] - A search query for the location finder
143
178
  * @param {Extent} [options.mapExtent] - A map extent to set
179
+ * @param {OperationMode} [options.operationMode] - The mode in which a workbook should be operated
144
180
  * @param {boolean} [options.useMapSrs] - Whether the geometry and the extent are in the map's SRS (otherwise EPSG:4326 is assumed)
145
181
  * @param {AbortSignal} [options.signal] - A signal to abort the iframe loading
146
182
  * @return {Promise<void>} A Promise for when the iframe is loaded
147
- * @throws For an invalid workbook view source or geometry type
183
+ * @throws For invalid arguments
148
184
  */
149
- showMap(mapView, { geometry, hideMainHeaderAndFooter, hideWorkbookToolBar, locationFinder, mapExtent, useMapSrs, signal, } = {}) {
185
+ async showMap(mapView, { disabledUiFeatures, geometry, hideMainHeaderAndFooter, hideWorkbookToolBar, highlightGlobalId, locationFinder, mapExtent, operationMode, useMapSrs, signal, } = {}) {
150
186
  this.#log('CadenzaClient#showMap', mapView, geometry);
151
187
  if (geometry) {
152
188
  assertValidGeometryType(geometry.type);
153
189
  }
154
- const params = new URLSearchParams({
155
- ...(hideMainHeaderAndFooter && { hideMainHeaderAndFooter: 'true' }),
156
- ...(hideWorkbookToolBar && { hideWorkbookToolBar: 'true' }),
157
- ...(locationFinder && { locationFinder }),
158
- ...(mapExtent && { mapExtent: mapExtent.join() }),
159
- ...(useMapSrs && { useMapSrs: 'true' }),
190
+ const params = createParams({
191
+ disabledUiFeatures,
192
+ hideMainHeaderAndFooter,
193
+ hideWorkbookToolBar,
194
+ highlightGlobalId,
195
+ locationFinder,
196
+ mapExtent,
197
+ operationMode,
198
+ useMapSrs,
199
+ webApplication: this.#webApplication,
160
200
  });
161
- return this.#show(resolvePath(mapView), { params, signal }).then(() => this.#postEvent('setGeometry', { geometry }));
201
+ await this.#show(resolvePath(mapView), params, signal);
202
+ this.#postEvent('setGeometry', { geometry });
162
203
  }
163
204
  /**
164
205
  * Create a geometry.
@@ -169,29 +210,29 @@ export class CadenzaClient {
169
210
  * @param {WorkbookViewSource} backgroundMapView - The workbook map view in the background
170
211
  * @param {GeometryType} geometryType - The geometry type
171
212
  * @param {object} [options] - Options
172
- * @param {Extent} [options.mapExtent] - A map extent to set
173
213
  * @param {string} [options.locationFinder] - A search query for the location finder
174
- * @param {boolean} [options.useMapSrs] - Whether the created geometry should use the map's SRS (otherwise EPSG:4326 will be used)
214
+ * @param {Extent} [options.mapExtent] - A map extent to set
175
215
  * @param {number} [options.minScale] - The minimum scale where the user should work on. A warning is shown when the map is zoomed out above the threshold.
216
+ * @param {boolean} [options.useMapSrs] - Whether the created geometry should use the map's SRS (otherwise EPSG:4326 will be used)
176
217
  * @param {AbortSignal} [options.signal] - A signal to abort the iframe loading
177
218
  * @return {Promise<void>} A Promise for when the iframe is loaded
178
- * @throws For an invalid workbook view source or geometry type
219
+ * @throws For invalid arguments
179
220
  * @fires `editGeometry:update` - When the user changed the geometry. The event includes the edited geometry.
180
221
  * @fires `editGeometry:ok` - When the user completed the geometry editing. The event includes the edited geometry.
181
222
  * @fires `editGeometry:cancel` - When the user cancelled the geometry editing in Cadenza.
182
223
  */
183
- createGeometry(backgroundMapView, geometryType, { mapExtent, locationFinder, useMapSrs, minScale, signal } = {}) {
224
+ createGeometry(backgroundMapView, geometryType, { locationFinder, mapExtent, minScale, useMapSrs, signal } = {}) {
184
225
  this.#log('CadenzaClient#createGeometry', backgroundMapView, geometryType);
185
- assertValidGeometryType(geometryType);
186
- const params = new URLSearchParams({
226
+ const params = createParams({
187
227
  action: 'editGeometry',
188
228
  geometryType,
189
- ...(useMapSrs && { useMapSrs: 'true' }),
190
- ...(locationFinder && { locationFinder }),
191
- ...(mapExtent && { mapExtent: mapExtent.join() }),
192
- ...(minScale && { minScale: String(minScale) }),
229
+ locationFinder,
230
+ mapExtent,
231
+ minScale,
232
+ useMapSrs,
233
+ webApplication: this.#webApplication,
193
234
  });
194
- return this.#show(resolvePath(backgroundMapView), { params, signal });
235
+ return this.#show(resolvePath(backgroundMapView), params, signal);
195
236
  }
196
237
  /**
197
238
  * Edit a geometry.
@@ -201,48 +242,39 @@ export class CadenzaClient {
201
242
  * @param {object} [options] - Options
202
243
  * @param {string} [options.locationFinder] - A search query for the location finder
203
244
  * @param {Extent} [options.mapExtent] - A map extent to set
204
- * @param {boolean} [options.useMapSrs] - Whether the geometry is in the map's SRS (otherwise EPSG:4326 is assumed)
205
245
  * @param {number} [options.minScale] - The minimum scale where the user should work on. A warning is shown when the map is zoomed out above the threshold.
246
+ * @param {boolean} [options.useMapSrs] - Whether the geometry is in the map's SRS (otherwise EPSG:4326 is assumed)
206
247
  * @param {AbortSignal} [options.signal] - A signal to abort the iframe loading
207
248
  * @return {Promise<void>} A Promise for when the iframe is loaded
208
- * @throws For an invalid workbook view source
249
+ * @throws For invalid arguments
209
250
  * @fires `editGeometry:update` - When the user changed the geometry. The event includes the edited geometry.
210
251
  * @fires `editGeometry:ok` - When the user completed the geometry editing. The event includes the edited geometry.
211
252
  * @fires `editGeometry:cancel` - When the user cancelled the geometry editing in Cadenza.
212
253
  */
213
- editGeometry(backgroundMapView, geometry, { mapExtent, locationFinder, useMapSrs, minScale, signal } = {}) {
254
+ async editGeometry(backgroundMapView, geometry, { locationFinder, mapExtent, minScale, useMapSrs, signal } = {}) {
214
255
  this.#log('CadenzaClient#editGeometry', backgroundMapView, geometry);
215
256
  assertValidGeometryType(geometry.type);
216
- const params = new URLSearchParams({
257
+ const params = createParams({
217
258
  action: 'editGeometry',
218
- ...(locationFinder && { locationFinder }),
219
- ...(mapExtent && { mapExtent: mapExtent.join() }),
220
- ...(useMapSrs && { useMapSrs: 'true' }),
221
- ...(minScale && { minScale: String(minScale) }),
259
+ locationFinder,
260
+ mapExtent,
261
+ minScale,
262
+ useMapSrs,
263
+ webApplication: this.#webApplication,
222
264
  });
223
- return this.#show(resolvePath(backgroundMapView), { params, signal }).then(() => this.#postEvent('setGeometry', { geometry }));
265
+ await this.#show(resolvePath(backgroundMapView), params, signal);
266
+ this.#postEvent('setGeometry', { geometry });
224
267
  }
225
- /**
226
- * @param {string} path
227
- * @param {object} options
228
- * @param {URLSearchParams} [options.params]
229
- * @param {AbortSignal} [options.signal]
230
- */
231
- #show(path, { params, signal }) {
232
- const url = new URL(this.baseUrl + path);
233
- if (params) {
234
- for (const [param, value] of params) {
235
- url.searchParams.append(param, value);
236
- }
237
- }
268
+ #show(
269
+ /** @type string */ path,
270
+ /** @type URLSearchParams */ params,
271
+ /** @type AbortSignal | undefined */ signal) {
272
+ const url = this.#createUrl(path, params);
238
273
  this.#log('Load iframe', url.toString());
239
274
  this.#requiredIframe.src = url.toString();
240
275
  return this.#getIframePromise(signal);
241
276
  }
242
- /**
243
- * @param {AbortSignal} [signal]
244
- */
245
- #getIframePromise(signal) {
277
+ #getIframePromise(/** @type AbortSignal | undefined */ signal) {
246
278
  const iframe = this.#requiredIframe;
247
279
  /** @type {() => void} */
248
280
  let onerror;
@@ -269,8 +301,9 @@ export class CadenzaClient {
269
301
  }),
270
302
  ];
271
303
  });
272
- promise.then(() => this.#log('Iframe loaded'), (error) => this.#log('Iframe loading failed', error));
273
- promise.finally(() => {
304
+ promise
305
+ .then(() => this.#log('Iframe loaded'), (error) => this.#log('Iframe loading failed', error))
306
+ .finally(() => {
274
307
  iframe.removeEventListener('error', onerror);
275
308
  signal?.removeEventListener('abort', onabort);
276
309
  unsubscribes.forEach((unsubscribe) => unsubscribe());
@@ -303,9 +336,8 @@ export class CadenzaClient {
303
336
  }
304
337
  };
305
338
  }
306
- /** @param {MessageEvent<CadenzaEvent<never>>} event */
307
339
  // Use arrow function so that it's bound to this.
308
- #onMessage = (event) => {
340
+ #onMessage = (/** @type MessageEvent<CadenzaEvent<never>> */ event) => {
309
341
  this.#log('Received message', event);
310
342
  if (event.origin !== this.#origin ||
311
343
  event.source !== this.#requiredIframe.contentWindow) {
@@ -318,25 +350,98 @@ export class CadenzaClient {
318
350
  }
319
351
  });
320
352
  };
321
- /**
322
- * @param {string} type
323
- * @param {unknown} detail
324
- */
325
- #postEvent(type, detail) {
353
+ #postEvent(/** @type string */ type, /** @type unknown */ detail) {
326
354
  const event = { type, detail };
327
355
  this.#log('postMessage', event);
328
356
  const contentWindow = /** @type {WindowProxy} */ (this.#requiredIframe.contentWindow);
329
357
  contentWindow.postMessage(event, { targetOrigin: this.#origin });
330
358
  }
331
- /** @param {unknown[]} args */
332
- #log(...args) {
359
+ /**
360
+ * Fetch data from a workbook view.
361
+ *
362
+ * @param {WorkbookViewSource} source - The workbook view to fetch data from
363
+ * @param {MediaType} mediaType - The media type to use for the data. Allowed are:
364
+ * * 'application/json'
365
+ * * 'application/pdf' (for Jasper Report views)
366
+ * * 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' (Excel)
367
+ * * 'text/csv'
368
+ * @param {object} options - Options
369
+ * @param {AbortSignal} [options.signal] - A signal to abort the data fetching
370
+ * @return {Promise<Response>} A Promise for the fetch response
371
+ * @throws For invalid arguments
372
+ */
373
+ fetchData(source, mediaType, { signal } = {}) {
374
+ this.#log('CadenzaClient#fetchData', source, mediaType);
375
+ assertSupportedMediaType(mediaType);
376
+ const params = createParams({ mediaType });
377
+ return this.#fetch(resolvePath(source), params, signal);
378
+ }
379
+ async #fetch(
380
+ /** @type string */ path,
381
+ /** @type URLSearchParams */ params,
382
+ /** @type AbortSignal | undefined */ signal) {
383
+ const url = this.#createUrl(path, params);
384
+ this.#log('Fetch', url.toString());
385
+ const res = await fetch(url, { signal });
386
+ if (!res.ok) {
387
+ const errorType = {
388
+ 400: 'bad-request',
389
+ 401: 'unauthorized',
390
+ 404: 'not-found',
391
+ }[res.status] ?? 'internal-error';
392
+ throw new CadenzaError(errorType, 'Failed to fetch data');
393
+ }
394
+ return res;
395
+ }
396
+ /**
397
+ * Download data from a workbook view.
398
+ *
399
+ * _Note:_ The file name, if not provided, is generated from the name of the workbook view and the current date.
400
+ *
401
+ * @param {WorkbookViewSource} source - The workbook view to download data from
402
+ * @param {MediaType} mediaType - The media type to use for the data. Allowed are:
403
+ * * 'application/json'
404
+ * * 'application/pdf' (for Jasper Report views)
405
+ * * 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' (Excel)
406
+ * * 'text/csv'
407
+ * @param {object} options - Options
408
+ * @param {string} [options.fileName] - The file name to use; The file extension is appended by Cadenza.
409
+ * @throws For invalid arguments
410
+ */
411
+ downloadData(source, mediaType, { fileName }) {
412
+ this.#log('CadenzaClient#downloadData', source, mediaType);
413
+ assertSupportedMediaType(mediaType);
414
+ const params = createParams({ fileName, mediaType });
415
+ this.#download(resolvePath(source), params);
416
+ }
417
+ #download(/** @type string */ path, /** @type URLSearchParams */ params) {
418
+ const url = this.#createUrl(path, params);
419
+ const a = document.createElement('a');
420
+ a.href = url.toString();
421
+ // causes the file to be downloaded even if the server sends a "Content-disposition: inline" header
422
+ a.download = '';
423
+ a.hidden = true;
424
+ document.body.append(a);
425
+ a.click();
426
+ a.remove();
427
+ }
428
+ #createUrl(/** @type string */ path, /** @type URLSearchParams */ params) {
429
+ const url = new URL(this.baseUrl + path);
430
+ if (params) {
431
+ for (const [param, value] of params) {
432
+ url.searchParams.append(param, value);
433
+ }
434
+ }
435
+ return url;
436
+ }
437
+ #log(/** @type unknown[] */ ...args) {
333
438
  if (this.#debug) {
334
439
  console.log(...args);
335
440
  }
336
441
  }
337
442
  }
338
- /** @param {WorkbookSource | WorksheetSource | WorkbookViewSource} source */
339
- function resolvePath(source) {
443
+ function resolvePath(
444
+ /** @type WorkbookSource | WorksheetSource | WorkbookViewSource */ source) {
340
445
  if (typeof source === 'string') {
341
446
  assert(validEmbeddingTargetId(source), `Invalid embedding target ID: ${source}`);
342
447
  return `/w/${source}`;
@@ -359,17 +464,12 @@ function resolvePath(source) {
359
464
  return path;
360
465
  }
361
466
  }
362
- /**
363
- * @param {boolean} assertion
364
- * @param {string} message
365
- */
366
- function assert(assertion, message) {
467
+ function assert(/** @type boolean */ assertion, /** @type string */ message) {
367
468
  if (!assertion) {
368
469
  throw new Error(message);
369
470
  }
370
471
  }
371
- /** @param {string} value */
372
- function validUrl(value) {
472
+ function validUrl(/** @type string */ value) {
373
473
  try {
374
474
  new URL(value);
375
475
  return true;
@@ -378,16 +478,13 @@ function validUrl(value) {
378
478
  return false;
379
479
  }
380
480
  }
381
- /** @param {string} value */
382
- function validEmbeddingTargetId(value) {
481
+ function validEmbeddingTargetId(/** @type string */ value) {
383
482
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
384
483
  }
385
- /** @param {string} value */
386
- function validRepositoryName(value) {
484
+ function validRepositoryName(/** @type string */ value) {
387
485
  return /^[\w -]{1,255}$/.test(value);
388
486
  }
389
- /** @param {string} value */
390
- function validWorkbookId(value) {
487
+ function validWorkbookId(/** @type string */ value) {
391
488
  try {
392
489
  // Workbook IDs are url-safe base64 strings.
393
490
  // https://stackoverflow.com/a/44528376
@@ -398,12 +495,14 @@ function validWorkbookId(value) {
398
495
  return false;
399
496
  }
400
497
  }
401
- /** @param {string} value */
402
- function assertValidGeometryType(value) {
403
- assert(validGeometryType(value), 'Invalid geometry type');
498
+ function validExternalLinkKey(/** @type ExternalLinkKey */ linkKey) {
499
+ return (validRepositoryName(linkKey.repositoryName) &&
500
+ validWorkbookId(linkKey.externalLinkId));
501
+ }
502
+ function assertValidGeometryType(/** @type string */ value) {
503
+ assert(validGeometryType(value), `Invalid geometry type: ${value}`);
404
504
  }
405
- /** @param {string} value */
406
- function validGeometryType(value) {
505
+ function validGeometryType(/** @type string */ value) {
407
506
  return [
408
507
  'Point',
409
508
  'MultiPoint',
@@ -413,6 +512,78 @@ function validGeometryType(value) {
413
512
  'MultiPolygon',
414
513
  ].includes(value);
415
514
  }
515
+ function validOperationMode(/** @type string */ value) {
516
+ return ['normal', 'simplified'].includes(value);
517
+ }
518
+ function validUiFeature(/** @type string */ value) {
519
+ return ['workbook-design', 'workbook-view-management'].includes(value);
520
+ }
521
+ /**
522
+ * @typedef {string} MediaType - A media type
523
+ *
524
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types}
525
+ */
526
+ const MediaType = /** @type {Record<string, MediaType>} */ {
527
+ CSV: 'text/csv',
528
+ EXCEL: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
529
+ JSON: 'application/json',
530
+ PDF: 'application/pdf',
531
+ };
532
+ function assertSupportedMediaType(
533
+ /** @type MediaType */ type,
534
+ /** @type MediaType[] */ supportedTypes = Object.values(MediaType)) {
535
+ return assert(supportedTypes.includes(type), `Invalid media type: ${type}`);
536
+ }
537
+ /**
538
+ * @param {object} params - Options
539
+ * @param {string} [params.action]
540
+ * @param {UiFeature[]} [params.disabledUiFeatures]
541
+ * @param {string} [params.fileName]
542
+ * @param {GeometryType} [params.geometryType]
543
+ * @param {boolean} [params.hideMainHeaderAndFooter]
544
+ * @param {boolean} [params.hideWorkbookToolBar]
545
+ * @param {GlobalId} [params.highlightGlobalId]
546
+ * @param {string} [params.locationFinder]
547
+ * @param {Extent} [params.mapExtent]
548
+ * @param {string} [params.mediaType]
549
+ * @param {number} [params.minScale]
550
+ * @param {OperationMode} [params.operationMode]
551
+ * @param {boolean} [params.useMapSrs]
552
+ * @param {ExternalLinkKey} [params.webApplication]
553
+ * @return {URLSearchParams}
554
+ */
555
+ function createParams({ action, fileName, geometryType, hideMainHeaderAndFooter, hideWorkbookToolBar, highlightGlobalId, locationFinder, mapExtent, mediaType, minScale, useMapSrs, webApplication, operationMode, disabledUiFeatures, }) {
556
+ if (disabledUiFeatures) {
557
+ disabledUiFeatures.forEach((feature) => assert(validUiFeature(feature), `Invalid UI feature: ${feature}`));
558
+ }
559
+ if (geometryType) {
560
+ assertValidGeometryType(geometryType);
561
+ }
562
+ if (operationMode) {
563
+ assert(validOperationMode(operationMode), `Invalid operation mode: ${operationMode}`);
564
+ }
565
+ return new URLSearchParams({
566
+ ...(action && { action }),
567
+ ...(disabledUiFeatures && {
568
+ disabledUiFeatures: disabledUiFeatures.join(),
569
+ }),
570
+ ...(fileName && { fileName }),
571
+ ...(geometryType && { geometryType }),
572
+ ...(hideMainHeaderAndFooter && { hideMainHeaderAndFooter: 'true' }),
573
+ ...(hideWorkbookToolBar && { hideWorkbookToolBar: 'true' }),
574
+ ...(highlightGlobalId && { highlightGlobalId }),
575
+ ...(locationFinder && { locationFinder }),
576
+ ...(mapExtent && { mapExtent: mapExtent.join() }),
577
+ ...(mediaType && { mediaType }),
578
+ ...(minScale && { minScale: String(minScale) }),
579
+ ...(operationMode && { operationMode }),
580
+ ...(useMapSrs && { useMapSrs: 'true' }),
581
+ ...(webApplication && {
582
+ webApplicationLink: webApplication.externalLinkId,
583
+ webApplicationLinkRepository: webApplication.repositoryName,
584
+ }),
585
+ });
586
+ }
416
587
  /**
417
588
  * @template [T=unknown]
418
589
  * @typedef CadenzaEvent - A Cadenza `postMessage()` event
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@disy/cadenza.js",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
+ "license": "Apache-2.0",
5
+ "repository": "github:DisyInformationssysteme/cadenza.js",
4
6
  "type": "module",
5
7
  "main": "./cadenza.js",
6
8
  "types": "./cadenza.d.ts",
@@ -8,27 +10,32 @@
8
10
  "apidoc/**/*",
9
11
  "cadenza.js",
10
12
  "cadenza.d.ts",
11
- "CHANGELOG.md"
13
+ "CHANGELOG.md",
14
+ "sandbox.html"
12
15
  ],
13
16
  "scripts": {
14
- "build": "run-p build:*",
17
+ "build": "run-p validate:* build:*",
15
18
  "build:docs": "typedoc",
16
19
  "build:ts": "tsc",
17
- "reformat": "prettier ./src --write",
20
+ "validate:format": "prettier ./src --check",
21
+ "validate:changelog": "changelog --latest-release",
18
22
  "start": "run-p start:*",
19
23
  "start:docs": "npm run build:docs -- --watch",
20
24
  "start:test": "npm test -- --watch",
25
+ "start:ts": "npm run build:ts -- --watch",
21
26
  "start:reformat": "onchange \"src/**/*\" -- prettier {{changed}} --write",
22
27
  "test": "cross-env NODE_OPTIONS=--unhandled-rejections=warn jest",
23
28
  "deploy:docs": "gh-pages --dist apidoc",
24
- "sandbox": "npx http-server -a localhost -c-1 -d false --proxy http://localhost:8000 -o /sandbox.html"
29
+ "sandbox": "node ./sandbox.cjs"
25
30
  },
26
31
  "devDependencies": {
27
32
  "@types/jest": "29.5.3",
28
33
  "cross-env": "7.0.3",
29
34
  "gh-pages": "6.0.0",
35
+ "http-server": "14.1.1",
30
36
  "jest": "29.6.1",
31
37
  "jest-environment-jsdom": "29.6.1",
38
+ "keep-a-changelog": "2.3.0",
32
39
  "npm-run-all": "4.1.5",
33
40
  "onchange": "7.1.0",
34
41
  "prettier": "3.0.0",