@thoughtspot/visual-embed-sdk 1.53.1 → 1.53.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/auth.d.ts.map +1 -1
  3. package/cjs/src/auth.js +25 -6
  4. package/cjs/src/auth.js.map +1 -1
  5. package/cjs/src/auth.spec.js +40 -8
  6. package/cjs/src/auth.spec.js.map +1 -1
  7. package/cjs/src/contracts/contracts.spec.js +165 -2
  8. package/cjs/src/contracts/contracts.spec.js.map +1 -1
  9. package/cjs/src/contracts/embed-event-payloads.d.ts +212 -14
  10. package/cjs/src/contracts/embed-event-payloads.d.ts.map +1 -1
  11. package/cjs/src/contracts/host-event-contracts.d.ts +1 -0
  12. package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
  13. package/cjs/src/css-variables.d.ts +140 -0
  14. package/cjs/src/css-variables.d.ts.map +1 -1
  15. package/cjs/src/embed/auto-frame-renderer.d.ts +14 -1
  16. package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -1
  17. package/cjs/src/embed/auto-frame-renderer.js +91 -2
  18. package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
  19. package/cjs/src/embed/auto-frame-renderer.spec.js +112 -7
  20. package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
  21. package/cjs/src/embed/host-events.spec.js +18 -0
  22. package/cjs/src/embed/host-events.spec.js.map +1 -1
  23. package/cjs/src/embed/liveboard.spec.js +42 -0
  24. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  25. package/cjs/src/react/util.d.ts +8 -1
  26. package/cjs/src/react/util.d.ts.map +1 -1
  27. package/cjs/src/react/util.js.map +1 -1
  28. package/cjs/src/types.d.ts +181 -1
  29. package/cjs/src/types.d.ts.map +1 -1
  30. package/cjs/src/types.js +180 -0
  31. package/cjs/src/types.js.map +1 -1
  32. package/dist/{index-CrBnZBZv.js → index-DQ7r5f1z.js} +1 -1
  33. package/dist/src/auth.d.ts.map +1 -1
  34. package/dist/src/contracts/embed-event-payloads.d.ts +212 -14
  35. package/dist/src/contracts/embed-event-payloads.d.ts.map +1 -1
  36. package/dist/src/contracts/host-event-contracts.d.ts +1 -0
  37. package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
  38. package/dist/src/css-variables.d.ts +140 -0
  39. package/dist/src/css-variables.d.ts.map +1 -1
  40. package/dist/src/embed/auto-frame-renderer.d.ts +14 -1
  41. package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -1
  42. package/dist/src/react/util.d.ts +8 -1
  43. package/dist/src/react/util.d.ts.map +1 -1
  44. package/dist/src/types.d.ts +181 -1
  45. package/dist/src/types.d.ts.map +1 -1
  46. package/dist/tsembed-react.es.js +230 -9
  47. package/dist/tsembed-react.js +229 -8
  48. package/dist/tsembed.es.js +298 -10
  49. package/dist/tsembed.js +297 -9
  50. package/dist/visual-embed-sdk-react-full.d.ts +534 -13
  51. package/dist/visual-embed-sdk-react.d.ts +531 -11
  52. package/dist/visual-embed-sdk.d.ts +527 -12
  53. package/lib/package.json +1 -1
  54. package/lib/src/auth.d.ts.map +1 -1
  55. package/lib/src/auth.js +25 -6
  56. package/lib/src/auth.js.map +1 -1
  57. package/lib/src/auth.spec.js +40 -8
  58. package/lib/src/auth.spec.js.map +1 -1
  59. package/lib/src/contracts/contracts.spec.js +165 -2
  60. package/lib/src/contracts/contracts.spec.js.map +1 -1
  61. package/lib/src/contracts/embed-event-payloads.d.ts +212 -14
  62. package/lib/src/contracts/embed-event-payloads.d.ts.map +1 -1
  63. package/lib/src/contracts/host-event-contracts.d.ts +1 -0
  64. package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
  65. package/lib/src/css-variables.d.ts +140 -0
  66. package/lib/src/css-variables.d.ts.map +1 -1
  67. package/lib/src/embed/auto-frame-renderer.d.ts +14 -1
  68. package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -1
  69. package/lib/src/embed/auto-frame-renderer.js +91 -2
  70. package/lib/src/embed/auto-frame-renderer.js.map +1 -1
  71. package/lib/src/embed/auto-frame-renderer.spec.js +113 -8
  72. package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
  73. package/lib/src/embed/host-events.spec.js +18 -0
  74. package/lib/src/embed/host-events.spec.js.map +1 -1
  75. package/lib/src/embed/liveboard.spec.js +42 -0
  76. package/lib/src/embed/liveboard.spec.js.map +1 -1
  77. package/lib/src/react/util.d.ts +8 -1
  78. package/lib/src/react/util.d.ts.map +1 -1
  79. package/lib/src/react/util.js.map +1 -1
  80. package/lib/src/types.d.ts +181 -1
  81. package/lib/src/types.d.ts.map +1 -1
  82. package/lib/src/types.js +180 -0
  83. package/lib/src/types.js.map +1 -1
  84. package/lib/src/visual-embed-sdk.d.ts +527 -12
  85. package/package.json +1 -1
  86. package/src/auth.spec.ts +47 -11
  87. package/src/auth.ts +35 -6
  88. package/src/contracts/contracts.spec.ts +182 -3
  89. package/src/contracts/embed-event-payloads.ts +281 -14
  90. package/src/contracts/host-event-contracts.ts +1 -0
  91. package/src/css-variables.ts +168 -0
  92. package/src/embed/auto-frame-renderer.spec.ts +134 -8
  93. package/src/embed/auto-frame-renderer.ts +103 -2
  94. package/src/embed/host-events.spec.ts +24 -0
  95. package/src/embed/liveboard.spec.ts +45 -0
  96. package/src/react/util.ts +13 -1
  97. package/src/types.ts +181 -0
@@ -1,4 +1,4 @@
1
- import { startAutoMCPFrameRenderer } from './auto-frame-renderer';
1
+ import { startAutoMCPFrameRenderer, HiddenActionItemByDefaultForMCPAnswers } from './auto-frame-renderer';
2
2
  import { Action, AuthType, AutoMCPFrameRendererViewConfig, EmbedEvent, InterceptedApiType, Param } from '../types';
3
3
  import { init } from '../index';
4
4
  import * as authInstance from '../auth';
@@ -266,9 +266,15 @@ describe('startAutoMCPFrameRenderer', () => {
266
266
  expect(src).toContain('disableHint');
267
267
  });
268
268
 
269
- test('hiddenActions → hideAction in rendered src as JSON array', async () => {
269
+ test('hiddenActions → hideAction in rendered src as JSON array (unioned with MCP denylist)', async () => {
270
270
  const src = await captureRenderedSrc({ hiddenActions: [Action.Pin] });
271
- expect(src).toContain(`hideAction=${JSON.stringify([Action.ReportError, Action.Pin])}`);
271
+ expect(src).toContain(
272
+ `hideAction=${JSON.stringify([
273
+ Action.ReportError,
274
+ Action.Pin,
275
+ ...HiddenActionItemByDefaultForMCPAnswers,
276
+ ])}`,
277
+ );
272
278
  });
273
279
 
274
280
  test('visibleActions → visibleAction in rendered src', async () => {
@@ -399,6 +405,95 @@ describe('startAutoMCPFrameRenderer', () => {
399
405
  });
400
406
  });
401
407
 
408
+ // ─── MCP-answer default hidden actions (denylist)
409
+ // ───────────────────────────────────────────────
410
+
411
+ describe('MCP-answer default hidden actions', () => {
412
+ /** Parse the hideAction JSON array out of a rendered iframe src. */
413
+ function parseHideAction(src: string): string[] {
414
+ const raw = decodeURIComponent(src.split('hideAction=')[1]?.split('&')[0] ?? '');
415
+ return raw ? JSON.parse(raw) : [];
416
+ }
417
+
418
+ /** Actions that must stay visible for MCP-rendered answers. */
419
+ const shouldStayVisible = [
420
+ Action.Pin,
421
+ Action.Save,
422
+ Action.Download,
423
+ Action.AxisMenuAggregate,
424
+ Action.AxisMenuFilter,
425
+ Action.AxisMenuSort,
426
+ Action.AxisMenuPosition,
427
+ Action.DrillExclude,
428
+ Action.DrillInclude,
429
+ Action.DrillDown,
430
+ Action.ShowUnderlyingData,
431
+ ];
432
+
433
+ test('empty config hides the full MCP denylist by default', async () => {
434
+ const src = await captureRenderedSrc();
435
+ const hidden = parseHideAction(src);
436
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
437
+ expect(hidden).toContain(action));
438
+ });
439
+
440
+ test('empty config keeps the curated visible actions visible', async () => {
441
+ const src = await captureRenderedSrc();
442
+ const hidden = parseHideAction(src);
443
+ shouldStayVisible.forEach((action) => expect(hidden).not.toContain(action));
444
+ });
445
+
446
+ test('caller hiddenActions are unioned with the MCP denylist', async () => {
447
+ const src = await captureRenderedSrc({ hiddenActions: [Action.Share] });
448
+ const hidden = parseHideAction(src);
449
+ expect(hidden).toContain(Action.Share);
450
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
451
+ expect(hidden).toContain(action));
452
+ });
453
+
454
+ test('string hideAction from additionalFlags is parsed, not spread into chars', async () => {
455
+ const src = await captureRenderedSrc({
456
+ additionalFlags: { hideAction: `${Action.Share}, ${Action.Edit}` },
457
+ });
458
+ const hidden = parseHideAction(src);
459
+ expect(hidden).toContain(Action.Share);
460
+ expect(hidden).toContain(Action.Edit);
461
+ hidden.forEach((action) => expect(action.length).toBeGreaterThan(1));
462
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
463
+ expect(hidden).toContain(action));
464
+ });
465
+
466
+ test('allowlist wins: visibleActions skips the MCP denylist', async () => {
467
+ const src = await captureRenderedSrc({ visibleActions: [Action.Pin] });
468
+ expect(src).toContain('visibleAction');
469
+ const hidden = parseHideAction(src);
470
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
471
+ expect(hidden).not.toContain(action));
472
+ });
473
+
474
+ test('source-iframe hideAction is unioned, not clobbered', async () => {
475
+ let capturedSrc = '';
476
+ renderIFrameSpy.mockRestore();
477
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
478
+ .mockImplementation(async function (this: any, src: string) {
479
+ capturedSrc = src;
480
+ });
481
+
482
+ const observer = startAutoMCPFrameRenderer();
483
+ const iframe = document.createElement('iframe');
484
+ const sourceHide = encodeURIComponent(JSON.stringify([Action.Save]));
485
+ iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true&hideAction=${sourceHide}#/embed/answer`;
486
+ document.body.appendChild(iframe);
487
+ await new Promise((r) => setTimeout(r, 50));
488
+
489
+ const hidden = parseHideAction(capturedSrc);
490
+ expect(hidden).toContain(Action.Save);
491
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
492
+ expect(hidden).toContain(action));
493
+ observer.disconnect();
494
+ });
495
+ });
496
+
402
497
  // ─── frameParams forwarding
403
498
  // ───────────────────────────────────────────────
404
499
 
@@ -539,12 +634,37 @@ describe('startAutoMCPFrameRenderer', () => {
539
634
  document.body.appendChild(iframe);
540
635
  await new Promise((r) => setTimeout(r, 50));
541
636
 
637
+ // tsmcp must not survive on the rendered iframe: isTSMCPIframe would
638
+ // otherwise re-match the SDK's own output and cause an infinite
639
+ // replacement loop via the same MutationObserver.
542
640
  expect(capturedSrc).not.toContain(`${Param.Tsmcp}=true`);
543
641
  expect(capturedSrc).toContain('customParam=hello');
544
642
  expect(capturedSrc).toMatch(/\?[^#]+#/);
545
643
  observer.disconnect();
546
644
  });
547
645
 
646
+ test('should mark the rendered iframe with isMcpAnswerEmbed=true instead of tsmcp', async () => {
647
+ let capturedSrc = '';
648
+ renderIFrameSpy.mockRestore();
649
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
650
+ .mockImplementation(async function (this: any, src: string) {
651
+ capturedSrc = src;
652
+ });
653
+
654
+ const observer = startAutoMCPFrameRenderer();
655
+ const iframe = document.createElement('iframe');
656
+ iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/answer`;
657
+ document.body.appendChild(iframe);
658
+ await new Promise((r) => setTimeout(r, 50));
659
+
660
+ // The app-facing marker is present, but tsmcp (the SDK's own
661
+ // re-processing trigger) is not — this is what keeps the
662
+ // MutationObserver from looping on its own rendered output.
663
+ expect(capturedSrc).toContain(`${Param.IsMcpAnswerEmbed}=true`);
664
+ expect(capturedSrc).not.toContain(`${Param.Tsmcp}=true`);
665
+ observer.disconnect();
666
+ });
667
+
548
668
  test('should preserve hash from original iframe src', async () => {
549
669
  let capturedSrc = '';
550
670
  renderIFrameSpy.mockRestore();
@@ -639,8 +759,9 @@ describe('startAutoMCPFrameRenderer', () => {
639
759
  return new URLSearchParams(qs).get(param);
640
760
  }
641
761
 
762
+ // hiddenActions is covered separately below: the auto-renderer unions the
763
+ // MCP denylist into hideAction, so its value is a superset of LiveboardEmbed's.
642
764
  test.each([
643
- ['hiddenActions', { hiddenActions: [Action.Pin] }, 'hideAction'],
644
765
  ['disabledActions', { disabledActions: [Action.Pin] }, 'disableAction'],
645
766
  ['visibleActions', { visibleActions: [Action.Download] }, 'visibleAction'],
646
767
  ])(
@@ -658,7 +779,7 @@ describe('startAutoMCPFrameRenderer', () => {
658
779
  },
659
780
  );
660
781
 
661
- test('hideAction value is a JSON array, not CSV', async () => {
782
+ test('hideAction value is a JSON array, not CSV, and is a superset of LiveboardEmbed', async () => {
662
783
  const autoSrc = await captureRenderedSrc({ hiddenActions: [Action.Pin] });
663
784
  const liveboardSrc = await captureLiveboardSrc({ hiddenActions: [Action.Pin] });
664
785
 
@@ -668,9 +789,14 @@ describe('startAutoMCPFrameRenderer', () => {
668
789
  // Both must parse as a JSON array — not CSV like "reportError,pin"
669
790
  expect(() => JSON.parse(autoValue)).not.toThrow();
670
791
  expect(() => JSON.parse(liveboardValue)).not.toThrow();
671
- expect(Array.isArray(JSON.parse(autoValue))).toBe(true);
672
- expect(Array.isArray(JSON.parse(liveboardValue))).toBe(true);
673
- expect(JSON.parse(autoValue)).toEqual(JSON.parse(liveboardValue));
792
+ const autoParsed = JSON.parse(autoValue);
793
+ const liveboardParsed = JSON.parse(liveboardValue);
794
+ expect(Array.isArray(autoParsed)).toBe(true);
795
+ expect(Array.isArray(liveboardParsed)).toBe(true);
796
+ // Auto-renderer hides everything LiveboardEmbed does, plus the MCP denylist.
797
+ liveboardParsed.forEach((action: string) => expect(autoParsed).toContain(action));
798
+ HiddenActionItemByDefaultForMCPAnswers.forEach((action) =>
799
+ expect(autoParsed).toContain(action));
674
800
  });
675
801
  });
676
802
 
@@ -1,4 +1,4 @@
1
- import { AutoMCPFrameRendererViewConfig, Param } from "../types";
1
+ import { Action, AutoMCPFrameRendererViewConfig, Param } from "../types";
2
2
  import { TsEmbed } from "./ts-embed";
3
3
  import { getQueryParamString } from "../utils";
4
4
  import { tokenizedFetch } from "../tokenizedFetch";
@@ -35,6 +35,28 @@ interface AnswerSessionParams {
35
35
  acGenNo: string;
36
36
  }
37
37
 
38
+ /**
39
+ * Actions hidden by default on Answers rendered by the ThoughtSpot MCP server
40
+ * (iframes carrying the `tsmcp=true` marker). Applied automatically by
41
+ * {@link AutoFrameRenderer} so the auto-rendered answer exposes a curated set of
42
+ * controls: Pin/Save/Download at the top level, Aggregate/Filter/Sort/Position in
43
+ * the column menu, and Exclude/Only include/Drill down/Show underlying data in the
44
+ * axis menu remain visible, while the actions below are removed.
45
+ *
46
+ * This is a denylist merged into {@link Param.HideActions}; anything not listed
47
+ * here stays visible. It is skipped when the caller supplies a `visibleActions`
48
+ * allowlist (see {@link AutoFrameRenderer.getEmbedParamsObject}).
49
+ */
50
+ export const HiddenActionItemByDefaultForMCPAnswers = [
51
+ Action.Edit,
52
+ Action.InConversationTraining, // Add to Coaching
53
+ Action.AxisMenuConditionalFormat,
54
+ Action.AxisMenuRename,
55
+ Action.AxisMenuEdit,
56
+ Action.AxisMenuRemove,
57
+ Action.MakeACopy,
58
+ ];
59
+
38
60
 
39
61
  /**
40
62
  * Starts an automatic renderer that watches the DOM for iframes containing
@@ -219,6 +241,39 @@ function getAnswerIds(
219
241
  return request;
220
242
  }
221
243
 
244
+ /**
245
+ * Normalizes a `hideAction` value into an array of action ids. The SDK-side value
246
+ * is already an `Action[]`; a value coming off the source iframe URL is a string
247
+ * that may be a JSON array (`["edit","save"]`) or a comma-separated list.
248
+ */
249
+ function parseHideActions(value: unknown): string[] {
250
+ if (Array.isArray(value)) {
251
+ return value as string[];
252
+ }
253
+ if (typeof value !== 'string' || value === '') {
254
+ return [];
255
+ }
256
+ try {
257
+ const parsed = JSON.parse(value);
258
+ if (Array.isArray(parsed)) {
259
+ return parsed.map(String);
260
+ }
261
+ } catch (e) {
262
+ // Not JSON — fall back to a comma-separated list.
263
+ }
264
+ return value.split(',').map((s) => s.trim()).filter(Boolean);
265
+ }
266
+
267
+ /**
268
+ * Unions two `hideAction` values (SDK-computed and source-iframe) into a single
269
+ * de-duplicated array of action ids.
270
+ */
271
+ function unionHideActions(sdkValue: unknown, sourceValue: unknown): string[] {
272
+ return Array.from(
273
+ new Set([...parseHideActions(sdkValue), ...parseHideActions(sourceValue)]),
274
+ );
275
+ }
276
+
222
277
  function isTSMCPIframe(iframe: HTMLIFrameElement) {
223
278
  try {
224
279
  const url = new URL(iframe.src);
@@ -247,11 +302,45 @@ class AutoFrameRenderer extends TsEmbed {
247
302
  super(container, viewConfig);
248
303
  }
249
304
 
305
+ /**
306
+ * Extends the base embed params with the curated MCP-answer denylist
307
+ * ({@link HiddenActionItemByDefaultForMCPAnswers}) and a marker letting
308
+ * the ThoughtSpot app detect this answer was rendered via the MCP
309
+ * auto-frame-renderer ({@link Param.IsMcpAnswerEmbed}).
310
+ *
311
+ * Precedence — merge, allowlist wins:
312
+ * - When the caller supplies a `visibleActions` allowlist, the base already
313
+ * set {@link Param.VisibleActions}; the denylist is skipped so the allowlist
314
+ * fully controls visibility.
315
+ * - Otherwise the denylist is unioned onto {@link Param.HideActions}, which the
316
+ * base has already populated with the default hidden actions plus any
317
+ * caller-supplied `hiddenActions`.
318
+ */
319
+ protected getEmbedParamsObject() {
320
+ const queryParams = super.getEmbedParamsObject();
321
+ if (!queryParams[Param.VisibleActions]) {
322
+ // `additionalFlags` can override this with a non-array value, so
323
+ // normalize it rather than spreading it directly.
324
+ queryParams[Param.HideActions] = unionHideActions(
325
+ queryParams[Param.HideActions],
326
+ HiddenActionItemByDefaultForMCPAnswers,
327
+ );
328
+ }
329
+ queryParams[Param.IsMcpAnswerEmbed] = true;
330
+ return queryParams;
331
+ }
332
+
250
333
  /**
251
334
  * Builds the final iframe `src` by merging the SDK embed parameters
252
335
  * with the query parameters already present on the source iframe URL.
336
+ *
253
337
  * The `tsmcp` marker param is removed so it does not propagate to the
254
- * ThoughtSpot application.
338
+ * ThoughtSpot application. This is not just cosmetic: `tsmcp=true` is
339
+ * also how {@link isTSMCPIframe} recognizes an iframe as one this
340
+ * renderer must process. If the marker survived onto the iframe this
341
+ * class inserts, the same {@link MutationObserver} would immediately
342
+ * detect its own output as a new MCP iframe and re-process it, causing
343
+ * infinite recursive replacement. Do not re-add `tsmcp` here.
255
344
  *
256
345
  * @param sourceSrc - The original iframe's `src` URL string.
257
346
  * @param preserveSourceUrl - When true, return the source URL unchanged.
@@ -274,7 +363,19 @@ class AutoFrameRenderer extends TsEmbed {
274
363
  delete existingQueryParamsObject[Param.TsmcpConversationId];
275
364
  delete existingQueryParamsObject[Param.TsmcpAnswerIndex];
276
365
 
366
+ // Source-iframe params win over SDK params on merge. Hidden actions
367
+ // are the exception: union them so a `hideAction` on the source iframe
368
+ // does not clobber the SDK-computed denylist (and vice versa).
369
+ const mergedHideActions = unionHideActions(
370
+ queryParams[Param.HideActions],
371
+ existingQueryParamsObject[Param.HideActions],
372
+ );
373
+ delete existingQueryParamsObject[Param.HideActions];
374
+
277
375
  const mergedQueryParams = { ...queryParams, ...existingQueryParamsObject };
376
+ if (mergedHideActions.length) {
377
+ mergedQueryParams[Param.HideActions] = mergedHideActions;
378
+ }
278
379
  const mergedQueryParamsString = getQueryParamString(mergedQueryParams, true);
279
380
  const queryString = mergedQueryParamsString ? `?${mergedQueryParamsString}` : '';
280
381
  const hash = resolved
@@ -228,6 +228,30 @@ describe('HostEvent.OpenAddParameterModal', () => {
228
228
  });
229
229
  });
230
230
 
231
+ // ---------------------------------------------------------------------------
232
+ // 10e – OpenAddTabModal
233
+ // ---------------------------------------------------------------------------
234
+ describe('HostEvent.OpenAddTabModal', () => {
235
+ test('postMessage type is openAddTabModal (camelCase, no payload)', async () => {
236
+ mockMessageChannel();
237
+ const { lb, iframe } = await renderLiveboard();
238
+ await executeAfterWait(() => {
239
+ lb.trigger(HostEvent.OpenAddTabModal);
240
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
241
+ expect.objectContaining({ type: 'openAddTabModal' }),
242
+ thoughtSpotHost,
243
+ expect.anything(),
244
+ );
245
+ });
246
+ });
247
+
248
+ test('returns null when !isRendered', async () => {
249
+ const lb = unrenderedLiveboard();
250
+ const result = await lb.trigger(HostEvent.OpenAddTabModal);
251
+ expect(result).toBeNull();
252
+ });
253
+ });
254
+
231
255
  // ---------------------------------------------------------------------------
232
256
  // 11 – AddColumns
233
257
  // ---------------------------------------------------------------------------
@@ -194,6 +194,51 @@ describe('Liveboard/viz embed tests', () => {
194
194
  });
195
195
  });
196
196
 
197
+ test('should hide the LiveboardAddPanel action', async () => {
198
+ expect(Action.LiveboardAddPanel).toBe('liveboardAddPanel');
199
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
200
+ hiddenActions: [Action.LiveboardAddPanel],
201
+ ...defaultViewConfig,
202
+ liveboardId,
203
+ } as LiveboardViewConfig);
204
+ liveboardEmbed.render();
205
+ await executeAfterWait(() => {
206
+ expectUrlToHaveParamsWithValues(getIFrameSrc(), {
207
+ hideAction: JSON.stringify([Action.ReportError, Action.LiveboardAddPanel]),
208
+ });
209
+ });
210
+ });
211
+
212
+ test('should disable the LiveboardAddPanel action with a reason', async () => {
213
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
214
+ disabledActions: [Action.LiveboardAddPanel],
215
+ disabledActionReason: 'Action denied',
216
+ ...defaultViewConfig,
217
+ liveboardId,
218
+ } as LiveboardViewConfig);
219
+ liveboardEmbed.render();
220
+ await executeAfterWait(() => {
221
+ expectUrlToHaveParamsWithValues(getIFrameSrc(), {
222
+ disableAction: JSON.stringify([Action.LiveboardAddPanel]),
223
+ disableHint: 'Action denied',
224
+ });
225
+ });
226
+ });
227
+
228
+ test('should set LiveboardAddPanel in visible actions', async () => {
229
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
230
+ visibleActions: [Action.LiveboardAddPanel],
231
+ ...defaultViewConfig,
232
+ liveboardId,
233
+ } as LiveboardViewConfig);
234
+ liveboardEmbed.render();
235
+ await executeAfterWait(() => {
236
+ expectUrlToHaveParamsWithValues(getIFrameSrc(), {
237
+ visibleAction: JSON.stringify([Action.LiveboardAddPanel]),
238
+ });
239
+ });
240
+ });
241
+
197
242
  test('should set EditLiveboard and EditVisualization independently of each other and of Edit', async () => {
198
243
  expect(Action.Edit).toBe('edit');
199
244
  expect(Action.EditLiveboard).toBe('editLiveboard');
package/src/react/util.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  import { EmbedEvent, MessageCallback, AllEmbedViewConfig } from '../types';
2
+ import { EmbedEventPayload } from '../contracts/embed-event-payloads';
2
3
 
3
4
  interface EmbedViewConfig extends AllEmbedViewConfig, EmbedEventHandlers {}
4
5
 
5
- export type EmbedEventHandlers = { [key in keyof typeof EmbedEvent as `on${Capitalize<key>}`]?: MessageCallback };
6
+ /**
7
+ * `on{Event}` handler props for the React components. Each handler receives the
8
+ * payload typed for its event (the same {@link EmbedEventPayload} the imperative
9
+ * `embed.on()` delivers), so `onData`, `onVizPointClick`, etc. are per-event
10
+ * typed rather than the loose {@link MessageCallback}.
11
+ */
12
+ export type EmbedEventHandlers = {
13
+ [K in keyof typeof EmbedEvent as `on${Capitalize<K>}`]?: (
14
+ payload: EmbedEventPayload<(typeof EmbedEvent)[K]>,
15
+ responder?: (data: any) => void,
16
+ ) => void;
17
+ };
6
18
 
7
19
  export interface EmbedProps extends EmbedViewConfig {
8
20
  className?: string;