@thoughtspot/visual-embed-sdk 1.49.0 → 1.49.2

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 (143) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/css-variables.d.ts +17 -0
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts.map +1 -1
  5. package/cjs/src/embed/app.js +4 -1
  6. package/cjs/src/embed/app.js.map +1 -1
  7. package/cjs/src/embed/app.spec.js +97 -0
  8. package/cjs/src/embed/app.spec.js.map +1 -1
  9. package/cjs/src/embed/conversation.d.ts +8 -0
  10. package/cjs/src/embed/conversation.d.ts.map +1 -1
  11. package/cjs/src/embed/conversation.js +2 -1
  12. package/cjs/src/embed/conversation.js.map +1 -1
  13. package/cjs/src/embed/conversation.spec.js +12 -0
  14. package/cjs/src/embed/conversation.spec.js.map +1 -1
  15. package/cjs/src/embed/events.spec.js +5 -0
  16. package/cjs/src/embed/events.spec.js.map +1 -1
  17. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  18. package/cjs/src/embed/liveboard.js +4 -1
  19. package/cjs/src/embed/liveboard.js.map +1 -1
  20. package/cjs/src/embed/liveboard.spec.js +11 -0
  21. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  22. package/cjs/src/embed/ts-embed.d.ts +38 -0
  23. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  24. package/cjs/src/embed/ts-embed.js +168 -11
  25. package/cjs/src/embed/ts-embed.js.map +1 -1
  26. package/cjs/src/embed/ts-embed.spec.js +237 -0
  27. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  28. package/cjs/src/mixpanel-service.js +2 -2
  29. package/cjs/src/mixpanel-service.js.map +1 -1
  30. package/cjs/src/types.d.ts +87 -4
  31. package/cjs/src/types.d.ts.map +1 -1
  32. package/cjs/src/types.js +47 -3
  33. package/cjs/src/types.js.map +1 -1
  34. package/cjs/src/utils/logger.js +2 -2
  35. package/cjs/src/utils/logger.js.map +1 -1
  36. package/cjs/src/utils/processTrigger.d.ts.map +1 -1
  37. package/cjs/src/utils/processTrigger.js +6 -5
  38. package/cjs/src/utils/processTrigger.js.map +1 -1
  39. package/cjs/src/utils/processTrigger.spec.js +13 -0
  40. package/cjs/src/utils/processTrigger.spec.js.map +1 -1
  41. package/cjs/src/utils/sdk-version.d.ts +2 -0
  42. package/cjs/src/utils/sdk-version.d.ts.map +1 -0
  43. package/cjs/src/utils/sdk-version.js +7 -0
  44. package/cjs/src/utils/sdk-version.js.map +1 -0
  45. package/cjs/src/utils.d.ts +10 -0
  46. package/cjs/src/utils.d.ts.map +1 -1
  47. package/cjs/src/utils.js +23 -1
  48. package/cjs/src/utils.js.map +1 -1
  49. package/cjs/src/utils.spec.js +48 -0
  50. package/cjs/src/utils.spec.js.map +1 -1
  51. package/dist/{index-_UGCSSDR.js → index-CS7yJ2Jj.js} +1 -1
  52. package/dist/src/css-variables.d.ts +17 -0
  53. package/dist/src/css-variables.d.ts.map +1 -1
  54. package/dist/src/embed/app.d.ts.map +1 -1
  55. package/dist/src/embed/conversation.d.ts +8 -0
  56. package/dist/src/embed/conversation.d.ts.map +1 -1
  57. package/dist/src/embed/liveboard.d.ts.map +1 -1
  58. package/dist/src/embed/ts-embed.d.ts +38 -0
  59. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  60. package/dist/src/types.d.ts +87 -4
  61. package/dist/src/types.d.ts.map +1 -1
  62. package/dist/src/utils/processTrigger.d.ts.map +1 -1
  63. package/dist/src/utils/sdk-version.d.ts +2 -0
  64. package/dist/src/utils/sdk-version.d.ts.map +1 -0
  65. package/dist/src/utils.d.ts +10 -0
  66. package/dist/src/utils.d.ts.map +1 -1
  67. package/dist/tsembed-react.es.js +255 -23
  68. package/dist/tsembed-react.js +254 -22
  69. package/dist/tsembed.es.js +255 -23
  70. package/dist/tsembed.js +254 -22
  71. package/dist/visual-embed-sdk-react-full.d.ts +148 -3
  72. package/dist/visual-embed-sdk-react.d.ts +148 -3
  73. package/dist/visual-embed-sdk.d.ts +148 -3
  74. package/lib/package.json +1 -1
  75. package/lib/src/css-variables.d.ts +17 -0
  76. package/lib/src/css-variables.d.ts.map +1 -1
  77. package/lib/src/embed/app.d.ts.map +1 -1
  78. package/lib/src/embed/app.js +4 -1
  79. package/lib/src/embed/app.js.map +1 -1
  80. package/lib/src/embed/app.spec.js +97 -0
  81. package/lib/src/embed/app.spec.js.map +1 -1
  82. package/lib/src/embed/conversation.d.ts +8 -0
  83. package/lib/src/embed/conversation.d.ts.map +1 -1
  84. package/lib/src/embed/conversation.js +2 -1
  85. package/lib/src/embed/conversation.js.map +1 -1
  86. package/lib/src/embed/conversation.spec.js +12 -0
  87. package/lib/src/embed/conversation.spec.js.map +1 -1
  88. package/lib/src/embed/events.spec.js +5 -0
  89. package/lib/src/embed/events.spec.js.map +1 -1
  90. package/lib/src/embed/liveboard.d.ts.map +1 -1
  91. package/lib/src/embed/liveboard.js +4 -1
  92. package/lib/src/embed/liveboard.js.map +1 -1
  93. package/lib/src/embed/liveboard.spec.js +11 -0
  94. package/lib/src/embed/liveboard.spec.js.map +1 -1
  95. package/lib/src/embed/ts-embed.d.ts +38 -0
  96. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  97. package/lib/src/embed/ts-embed.js +168 -11
  98. package/lib/src/embed/ts-embed.js.map +1 -1
  99. package/lib/src/embed/ts-embed.spec.js +237 -0
  100. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  101. package/lib/src/mixpanel-service.js +1 -1
  102. package/lib/src/mixpanel-service.js.map +1 -1
  103. package/lib/src/types.d.ts +87 -4
  104. package/lib/src/types.d.ts.map +1 -1
  105. package/lib/src/types.js +47 -3
  106. package/lib/src/types.js.map +1 -1
  107. package/lib/src/utils/logger.js +1 -1
  108. package/lib/src/utils/logger.js.map +1 -1
  109. package/lib/src/utils/processTrigger.d.ts.map +1 -1
  110. package/lib/src/utils/processTrigger.js +6 -5
  111. package/lib/src/utils/processTrigger.js.map +1 -1
  112. package/lib/src/utils/processTrigger.spec.js +13 -0
  113. package/lib/src/utils/processTrigger.spec.js.map +1 -1
  114. package/lib/src/utils/sdk-version.d.ts +2 -0
  115. package/lib/src/utils/sdk-version.d.ts.map +1 -0
  116. package/lib/src/utils/sdk-version.js +3 -0
  117. package/lib/src/utils/sdk-version.js.map +1 -0
  118. package/lib/src/utils.d.ts +10 -0
  119. package/lib/src/utils.d.ts.map +1 -1
  120. package/lib/src/utils.js +21 -0
  121. package/lib/src/utils.js.map +1 -1
  122. package/lib/src/utils.spec.js +49 -1
  123. package/lib/src/utils.spec.js.map +1 -1
  124. package/lib/src/visual-embed-sdk.d.ts +148 -3
  125. package/package.json +1 -1
  126. package/src/css-variables.ts +20 -0
  127. package/src/embed/app.spec.ts +109 -0
  128. package/src/embed/app.ts +4 -0
  129. package/src/embed/conversation.spec.ts +17 -0
  130. package/src/embed/conversation.ts +10 -0
  131. package/src/embed/events.spec.ts +7 -0
  132. package/src/embed/liveboard.spec.ts +15 -0
  133. package/src/embed/liveboard.ts +4 -0
  134. package/src/embed/ts-embed.spec.ts +294 -0
  135. package/src/embed/ts-embed.ts +181 -10
  136. package/src/mixpanel-service.ts +1 -1
  137. package/src/types.ts +86 -3
  138. package/src/utils/logger.ts +1 -1
  139. package/src/utils/processTrigger.spec.ts +23 -0
  140. package/src/utils/processTrigger.ts +8 -6
  141. package/src/utils/sdk-version.ts +3 -0
  142. package/src/utils.spec.ts +63 -0
  143. package/src/utils.ts +24 -0
@@ -28,6 +28,7 @@ import {
28
28
  getCustomisations,
29
29
  getRuntimeFilters,
30
30
  getDOMNode,
31
+ querySelectorAcrossShadowRoot,
31
32
  getFilterQuery,
32
33
  getQueryParamString,
33
34
  getRuntimeParameters,
@@ -68,7 +69,7 @@ import {
68
69
  } from '../types';
69
70
  import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
70
71
  import { processEventData, processAuthFailure } from '../utils/processData';
71
- import { version } from '../../package.json';
72
+ import { version } from '../utils/sdk-version';
72
73
  import {
73
74
  getAuthPromise,
74
75
  renderInQueue,
@@ -95,6 +96,14 @@ import {
95
96
  */
96
97
  export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
97
98
  const TS_EMBED_ID = '_thoughtspot-embed';
99
+ /**
100
+ * dataset key used to stash a custom preRenderContainer's original inline
101
+ * `position` while we override it to `relative`. Stored on the container (not
102
+ * per-instance) so the override can be reverted on destroy even when multiple
103
+ * pre-rendered embeds share the same container.
104
+ */
105
+ const PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY = 'tsEmbedOriginalPosition';
106
+ const PRERENDER_WRAPPER_ID_PREFIX = 'tsEmbed-pre-render-wrapper-';
98
107
 
99
108
  /**
100
109
  * The event id map from v2 event names to v1 event id
@@ -197,6 +206,10 @@ export class TsEmbed {
197
206
 
198
207
  private resizeObserver: ResizeObserver;
199
208
 
209
+ private preRenderContainerEl: HTMLElement | null = null;
210
+
211
+ private containerScrollListener: (() => void) | null = null;
212
+
200
213
  protected hostEventClient: HostEventClient;
201
214
 
202
215
  protected isReadyForRenderPromise;
@@ -1152,6 +1165,91 @@ export class TsEmbed {
1152
1165
  return placeholder;
1153
1166
  }
1154
1167
 
1168
+ /**
1169
+ * Resolves the configured preRenderContainer to a live element, falling
1170
+ * back to `document.body`. A string selector is re-queried on every call so
1171
+ * a remounted container (E.g.: React replacing the node) resolves to the
1172
+ * fresh element; an element passed directly cannot be re-resolved.
1173
+ */
1174
+ private resolvePreRenderContainerTarget(): HTMLElement {
1175
+ const containerConfig = this.viewConfig.preRenderContainer;
1176
+ let container: Element | null = null;
1177
+ if (typeof containerConfig === 'string') {
1178
+ try {
1179
+ // Resolve against the host's shadow root too, so a selector can
1180
+ // target a container inside the same shadow DOM as the embed —
1181
+ // document.querySelector alone cannot pierce shadow boundaries.
1182
+ container = querySelectorAcrossShadowRoot(containerConfig, this.hostElement);
1183
+ } catch (e) {
1184
+ logger.error(`Invalid CSS selector for preRenderContainer: ${containerConfig}`, e);
1185
+ }
1186
+ } else if (containerConfig) {
1187
+ container = containerConfig;
1188
+ }
1189
+ return (container as HTMLElement) ?? document.body;
1190
+ }
1191
+
1192
+ /**
1193
+ * Makes the container a positioning context for the absolutely positioned
1194
+ * wrapper, stashing the original inline `position` on the element (once) so
1195
+ * destroy() can restore it exactly, leaving no trace. Recording it on the
1196
+ * element rather than per-instance lets the override be reverted even when
1197
+ * embeds share the same container.
1198
+ */
1199
+ private applyPreRenderContainerPositioning(container: HTMLElement): void {
1200
+ if (container === document.body) {
1201
+ return;
1202
+ }
1203
+ const pos = window.getComputedStyle(container).position;
1204
+ if (pos === 'static') {
1205
+ if (container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY] === undefined) {
1206
+ container.dataset[
1207
+ PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY
1208
+ ] = container.style.position;
1209
+ }
1210
+ container.style.position = 'relative';
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Re-attaches the wrapper to a live container when the previously resolved
1216
+ * one has been detached or no longer holds the wrapper — e.g. the host app
1217
+ * remounted a custom preRenderContainer, which would otherwise leave a stale
1218
+ * reference and collapse the wrapper. Only string selectors can be
1219
+ * re-resolved; a container passed as an element is left untouched.
1220
+ */
1221
+ private reconcilePreRenderContainer(): void {
1222
+ const wrapper = this.preRenderWrapper;
1223
+ const stored = this.preRenderContainerEl;
1224
+ // Nothing to reconcile until this instance resolved its container.
1225
+ if (!wrapper || !stored) {
1226
+ return;
1227
+ }
1228
+ const storedIsLive = stored === document.body || document.contains(stored);
1229
+ if (storedIsLive && stored.contains(wrapper)) {
1230
+ return;
1231
+ }
1232
+ const resolved = this.resolvePreRenderContainerTarget();
1233
+ // Re-resolution yielded the same (still stale) element — nothing we can
1234
+ // do, e.g. a detached container passed as an HTMLElement.
1235
+ if (resolved === stored && stored.contains(wrapper)) {
1236
+ return;
1237
+ }
1238
+ if (this.containerScrollListener && stored !== resolved) {
1239
+ if (stored !== document.body) {
1240
+ stored.removeEventListener('scroll', this.containerScrollListener);
1241
+ }
1242
+ if (resolved !== document.body) {
1243
+ resolved.addEventListener('scroll', this.containerScrollListener);
1244
+ }
1245
+ }
1246
+ this.applyPreRenderContainerPositioning(resolved);
1247
+ if (wrapper.parentNode !== resolved) {
1248
+ resolved.appendChild(wrapper);
1249
+ }
1250
+ this.preRenderContainerEl = resolved;
1251
+ }
1252
+
1155
1253
  protected insertIntoDOMForPreRender(child: string | Node): void {
1156
1254
  const preRenderChild = this.createPreRenderChild(child);
1157
1255
  const preRenderWrapper = this.createPreRenderWrapper();
@@ -1175,7 +1273,10 @@ export class TsEmbed {
1175
1273
  this.hidePreRender();
1176
1274
  }
1177
1275
 
1178
- document.body.appendChild(preRenderWrapper);
1276
+ const targetContainer = this.resolvePreRenderContainerTarget();
1277
+ this.preRenderContainerEl = targetContainer;
1278
+ this.applyPreRenderContainerPositioning(targetContainer);
1279
+ targetContainer.appendChild(preRenderWrapper);
1179
1280
  }
1180
1281
 
1181
1282
  private showPreRenderByDefault = false;
@@ -1735,6 +1836,55 @@ export class TsEmbed {
1735
1836
  return tsParams;
1736
1837
  }
1737
1838
 
1839
+ /**
1840
+ * Reverts the custom preRenderContainer's `position` to the value it had
1841
+ * before we overrode it to `relative` (see insertIntoDOMForPreRender).
1842
+ *
1843
+ * We restore the original inline value rather than forcing `static`, and we
1844
+ * skip the restore if another preRender wrapper is still mounted inside the
1845
+ * same container — a shared container still needs the positioning context.
1846
+ */
1847
+ private restorePreRenderContainerPosition(): void {
1848
+ const container = this.preRenderContainerEl as HTMLElement | null;
1849
+ if (!container || container === document.body) {
1850
+ return;
1851
+ }
1852
+ // Drop our reference up front so a destroyed embed never pins a
1853
+ // detached container in memory; restoration uses the local handle.
1854
+ this.preRenderContainerEl = null;
1855
+ const originalPosition = container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY];
1856
+ if (originalPosition === undefined) {
1857
+ // We never overrode this container's position; nothing to restore.
1858
+ return;
1859
+ }
1860
+ // This instance's own wrapper has already been removed by now, so any
1861
+ // match here belongs to another embed still sharing the container — it
1862
+ // continues to rely on the positioning context, so leave it in place.
1863
+ const hasOtherWrapper = container.querySelector(
1864
+ `[id^="${PRERENDER_WRAPPER_ID_PREFIX}"]`,
1865
+ );
1866
+ if (hasOtherWrapper) {
1867
+ return;
1868
+ }
1869
+ container.style.position = originalPosition;
1870
+ delete container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY];
1871
+ }
1872
+
1873
+ /**
1874
+ * Detaches and clears the container scroll listener, if one is attached.
1875
+ */
1876
+ private removeContainerScrollListener(): void {
1877
+ if (!this.containerScrollListener) {
1878
+ return;
1879
+ }
1880
+ const customContainer =
1881
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
1882
+ ? this.preRenderContainerEl
1883
+ : null;
1884
+ customContainer?.removeEventListener('scroll', this.containerScrollListener);
1885
+ this.containerScrollListener = null;
1886
+ }
1887
+
1738
1888
  /**
1739
1889
  * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
1740
1890
  * @version SDK: 1.19.1 | ThoughtSpot: *
@@ -1742,8 +1892,10 @@ export class TsEmbed {
1742
1892
  public destroy(): void {
1743
1893
  try {
1744
1894
  this.removeFullscreenChangeHandler();
1895
+ this.removeContainerScrollListener();
1745
1896
  this.unsubscribeToEvents();
1746
1897
  this.preRenderWrapper?.remove();
1898
+ this.restorePreRenderContainerPosition();
1747
1899
  if (!this.isRendered) {
1748
1900
  return;
1749
1901
  }
@@ -1856,15 +2008,21 @@ export class TsEmbed {
1856
2008
 
1857
2009
  this.syncPreRenderStyle();
1858
2010
 
2011
+ const customContainer =
2012
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
2013
+ ? this.preRenderContainerEl
2014
+ : null;
2015
+ if (customContainer && !this.containerScrollListener) {
2016
+ this.containerScrollListener = () => this.syncPreRenderStyle();
2017
+ customContainer.addEventListener('scroll', this.containerScrollListener);
2018
+ }
2019
+
1859
2020
  if (!this.viewConfig.doNotTrackPreRenderSize) {
1860
2021
  const observeTarget = (this.insertedDomEl as HTMLElement) ?? this.hostElement;
1861
2022
  this.resizeObserver = new ResizeObserver((entries) => {
1862
2023
  entries.forEach((entry) => {
1863
- if (entry.contentRect && entry.target === observeTarget) {
1864
- setStyleProperties(this.preRenderWrapper, {
1865
- width: `${entry.contentRect.width}px`,
1866
- height: `${entry.contentRect.height}px`,
1867
- });
2024
+ if (entry.target === observeTarget) {
2025
+ this.syncPreRenderStyle();
1868
2026
  }
1869
2027
  });
1870
2028
  });
@@ -1905,11 +2063,22 @@ export class TsEmbed {
1905
2063
  logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1906
2064
  return;
1907
2065
  }
2066
+ // Self-heal if the resolved container was remounted/detached, so we
2067
+ // never measure a stale node (which would collapse the wrapper).
2068
+ this.reconcilePreRenderContainer();
1908
2069
  const elBoundingClient = this.getPreRenderPlaceHolderElement().getBoundingClientRect();
1909
2070
 
2071
+ const containerEl =
2072
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
2073
+ ? this.preRenderContainerEl
2074
+ : null;
2075
+ const containerRect = containerEl?.getBoundingClientRect() ?? { x: 0, y: 0 };
2076
+ const scrollX = containerEl ? containerEl.scrollLeft : window.scrollX;
2077
+ const scrollY = containerEl ? containerEl.scrollTop : window.scrollY;
2078
+
1910
2079
  setStyleProperties(this.preRenderWrapper, {
1911
- top: `${elBoundingClient.y + window.scrollY}px`,
1912
- left: `${elBoundingClient.x + window.scrollX}px`,
2080
+ top: `${elBoundingClient.y - containerRect.y + scrollY}px`,
2081
+ left: `${elBoundingClient.x - containerRect.x + scrollX}px`,
1913
2082
  width: `${elBoundingClient.width}px`,
1914
2083
  height: `${elBoundingClient.height}px`,
1915
2084
  position: 'absolute',
@@ -1938,6 +2107,8 @@ export class TsEmbed {
1938
2107
  };
1939
2108
  setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
1940
2109
 
2110
+ this.removeContainerScrollListener();
2111
+
1941
2112
  if (this.resizeObserver) {
1942
2113
  this.resizeObserver.disconnect();
1943
2114
  }
@@ -1959,7 +2130,7 @@ export class TsEmbed {
1959
2130
  */
1960
2131
  public getPreRenderIds() {
1961
2132
  return {
1962
- wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
2133
+ wrapper: `${PRERENDER_WRAPPER_ID_PREFIX}${this.viewConfig.preRenderId}`,
1963
2134
  child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1964
2135
  placeHolder: `tsEmbed-pre-render-placeholder-${this.viewConfig.preRenderId}`,
1965
2136
  };
@@ -2,7 +2,7 @@ import * as mixpanel from 'mixpanel-browser';
2
2
  import { logger } from './utils/logger';
3
3
  import { SessionInfo } from './utils/sessionInfoService';
4
4
  import { ERROR_MESSAGE } from './errors';
5
- import { version } from '../package.json';
5
+ import { version } from './utils/sdk-version';
6
6
 
7
7
  export const EndPoints = {
8
8
  CONFIG: '/callosum/v1/system/config',
package/src/types.ts CHANGED
@@ -1108,6 +1108,45 @@ export interface BaseViewConfig extends ApiInterceptFlags {
1108
1108
  * ```
1109
1109
  */
1110
1110
  doNotTrackPreRenderSize?: boolean;
1111
+ /**
1112
+ * The DOM element or CSS selector string specifying the container into which
1113
+ * the pre-rendered wrapper is inserted. Defaults to `document.body` when not
1114
+ * specified.
1115
+ *
1116
+ * **When to use a target container:** set this when the pre-render should
1117
+ * live somewhere other than `document.body` — for example when `<body>` has
1118
+ * `overflow: hidden` and scrolling is handled by an inner element, or when
1119
+ * you need the wrapper to sit inside a specific stacking/positioning context.
1120
+ * The wrapper is positioned to track the embedding element within this
1121
+ * container, so choose the scrollable/positioned ancestor you want it
1122
+ * aligned to.
1123
+ *
1124
+ * **Pass a stable container:** the wrapper is mounted into the resolved
1125
+ * container once, during `preRender()`. The container must stay mounted for
1126
+ * the lifetime of the pre-render — if the host app unmounts and remounts it
1127
+ * (for example React replacing the node), the wrapper is orphaned on the
1128
+ * detached node. Mount the container above the part of the tree that
1129
+ * re-renders so its identity is stable. Prefer a CSS selector string over a
1130
+ * raw element: a selector can be re-resolved to the fresh node on the next
1131
+ * reposition, whereas an element reference cannot be recovered once detached.
1132
+ *
1133
+ * When the embed host lives inside a Shadow DOM, a selector string is
1134
+ * resolved against that shadow root as well (since `document.querySelector`
1135
+ * cannot pierce shadow boundaries); pass the element directly if the
1136
+ * container lives in a different root.
1137
+ *
1138
+ * @type {string | HTMLElement}
1139
+ * @version SDK: 1.49.2 | ThoughtSpot: *
1140
+ * @example
1141
+ * ```js
1142
+ * const embed = new LiveboardEmbed('#tsEmbed', {
1143
+ * preRenderId: 'my-liveboard',
1144
+ * // Prefer a selector for a stable, scrollable container.
1145
+ * preRenderContainer: '#my-scroll-container',
1146
+ * });
1147
+ * ```
1148
+ */
1149
+ preRenderContainer?: string | HTMLElement;
1111
1150
  /**
1112
1151
  * Enable the V2 shell. This can provide performance benefits
1113
1152
  * due to a lighter-weight shell.
@@ -4150,11 +4189,30 @@ export enum HostEvent {
4150
4189
  /**
4151
4190
  * Navigate to a specific page in the embedded ThoughtSpot application.
4152
4191
  * This is the same as calling `appEmbed.navigateToPage(path, true)`.
4153
- * @param - `path` - the path to navigate to go forward or back. The path value can
4154
- * be a number; for example, `1`, `-1`.
4192
+ *
4193
+ * Accepts either a plain value or an object with `path`
4194
+ * and an optional `replace` flag.
4195
+ *
4196
+ * @param data - A string path, a numeric history delta, or an object
4197
+ * `{ path: string | number, replace?: boolean }`.
4198
+ * - `path` — the route to navigate to, or a history delta such as `1`
4199
+ * or `-1` (calls `window.history.go()`).
4200
+ * - `replace` — when `true`, replaces the current history entry instead
4201
+ * of pushing a new one (uses `window.location.replace`).
4202
+ *
4155
4203
  * @example
4156
4204
  * ```js
4157
- * appEmbed.navigateToPage(-1)
4205
+ * // Preferred: use navigateToPage directly
4206
+ * appEmbed.navigateToPage(-1);
4207
+ *
4208
+ * // Numeric delta — go back one step
4209
+ * appEmbed.trigger(HostEvent.Navigate, -1);
4210
+ *
4211
+ * // String path — push a new history entry
4212
+ * appEmbed.trigger(HostEvent.Navigate, 'home');
4213
+ *
4214
+ * // Object format — replace current history entry
4215
+ * appEmbed.trigger(HostEvent.Navigate, { path: 'home', replace: true }); // SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
4158
4216
  * ```
4159
4217
  * @version SDK: 1.12.0 | ThoughtSpot: 8.4.0.cl, 8.4.1.sw
4160
4218
  */
@@ -6149,6 +6207,7 @@ export enum Param {
6149
6207
  EnableLiveboardDataCache = 'enableLiveboardDataCache',
6150
6208
  SpotterFileUploadEnabled = 'spotterFileUploadEnabled',
6151
6209
  SpotterFileUploadFileTypes = 'spotterFileUploadFileTypes',
6210
+ IsStarterPromptsEnabled = 'enableStarterPrompts',
6152
6211
  }
6153
6212
 
6154
6213
  /**
@@ -6801,6 +6860,16 @@ export enum Action {
6801
6860
  * @version SDK: 1.9.0 | ThoughtSpot: 8.1.0.cl, 8.4.1.sw
6802
6861
  */
6803
6862
  AddToFavorites = 'addToFavorites',
6863
+ /**
6864
+ * Remove from Favorites option in home page v3 sidebar.
6865
+ * Allows removing an object from the user's favorites list from home-page v3 sidebar.
6866
+ * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
6867
+ * @example
6868
+ * ```js
6869
+ * disabledActions: [Action.RemoveFromFavorites]
6870
+ * ```
6871
+ */
6872
+ RemoveFromFavorites = 'removeFromFavorites',
6804
6873
  /**
6805
6874
  * The edit icon on Liveboards (Classic experience).
6806
6875
  * @example
@@ -7879,6 +7948,20 @@ export enum Action {
7879
7948
  * ```
7880
7949
  */
7881
7950
  SpotterViz = 'spotterViz',
7951
+ /**
7952
+ * The reference-mode toggle button inside the SpotterViz chat input.
7953
+ * When users enable reference mode, clicking tiles, filters, or
7954
+ * parameters adds them as chat context. Hide this action to remove
7955
+ * the reference-mode capability entirely.
7956
+ * Visible by default.
7957
+ * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
7958
+ * @example
7959
+ * ```js
7960
+ * hiddenActions: [Action.SpotterVizReferenceMode]
7961
+ * disabledActions: [Action.SpotterVizReferenceMode]
7962
+ * ```
7963
+ */
7964
+ SpotterVizReferenceMode = 'spotterVizReferenceMode',
7882
7965
  /**
7883
7966
  * Clears browser cache and fetches new data for liveboard ChartViz Containers.
7884
7967
  * Requires `enableLiveboardDataCache` to be enabled.
@@ -1,5 +1,5 @@
1
1
  import { LogLevel } from '../types';
2
- import { version } from '../../package.json';
2
+ import { version } from './sdk-version';
3
3
  const logFunctions: {
4
4
  [key: string]: (...args: any[]) => void;
5
5
  } = {
@@ -94,6 +94,29 @@ describe('Unit test for processTrigger', () => {
94
94
  await expect(triggerPromise).resolves.toEqual(null);
95
95
  });
96
96
 
97
+ test('should clear the timeout when response arrives before TRIGGER_TIMEOUT', async () => {
98
+ const messageType = HostEvent.Search;
99
+ const thoughtSpotHost = 'http://localhost:3000';
100
+ const data = {};
101
+ mockMessageChannel();
102
+ const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
103
+
104
+ const triggerPromise = _processTriggerInstance.processTrigger(
105
+ iFrame,
106
+ messageType,
107
+ thoughtSpotHost,
108
+ data,
109
+ );
110
+
111
+ const res = { data: { test: '123' } };
112
+ messageChannelMock.port1.onmessage(res);
113
+
114
+ expect(clearTimeoutSpy).toHaveBeenCalled();
115
+ await expect(triggerPromise).resolves.toEqual(res.data);
116
+
117
+ clearTimeoutSpy.mockRestore();
118
+ });
119
+
97
120
  test('should close channel.port1 when timeout exceeds TRIGGER_TIMEOUT', async () => {
98
121
  const messageType = HostEvent.Search;
99
122
  const thoughtSpotHost = 'http://localhost:3000';
@@ -70,7 +70,15 @@ export function processTrigger(
70
70
  }
71
71
 
72
72
  const channel = new MessageChannel();
73
+
74
+ // Close the messageChannel and resolve the promise if timeout.
75
+ const timeoutId = setTimeout(() => {
76
+ channel.port1.close();
77
+ res(new Error(ERROR_MESSAGE.TRIGGER_TIMED_OUT));
78
+ }, TRIGGER_TIMEOUT);
79
+
73
80
  channel.port1.onmessage = ({ data: responseData }) => {
81
+ clearTimeout(timeoutId);
74
82
  channel.port1.close();
75
83
  const error = responseData?.error || responseData?.data?.error;
76
84
  if (error) {
@@ -80,12 +88,6 @@ export function processTrigger(
80
88
  }
81
89
  };
82
90
 
83
- // Close the messageChannel and resolve the promise if timeout.
84
- setTimeout(() => {
85
- channel.port1.close();
86
- res(new Error(ERROR_MESSAGE.TRIGGER_TIMED_OUT));
87
- }, TRIGGER_TIMEOUT);
88
-
89
91
  return postIframeMessage(iFrame, { type: messageType, data, context }, thoughtSpotHost, channel);
90
92
  });
91
93
  }
@@ -0,0 +1,3 @@
1
+ import pkg from '../../package.json';
2
+
3
+ export const { version } = pkg;
package/src/utils.spec.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  resetValueFromWindow,
27
27
  validateHttpUrl,
28
28
  setParamIfDefined,
29
+ querySelectorAcrossShadowRoot,
29
30
  } from './utils';
30
31
  import { RuntimeFilterOp } from './types';
31
32
  import { logger } from './utils/logger';
@@ -855,6 +856,68 @@ describe('getClippingAncestors', () => {
855
856
  });
856
857
  });
857
858
 
859
+ describe('querySelectorAcrossShadowRoot', () => {
860
+ it('should resolve a selector from the light DOM document', () => {
861
+ const el = document.createElement('div');
862
+ el.id = 'light-dom-target';
863
+ document.body.appendChild(el);
864
+
865
+ expect(querySelectorAcrossShadowRoot('#light-dom-target')).toBe(el);
866
+
867
+ el.remove();
868
+ });
869
+
870
+ it('should resolve a selector inside the reference node shadow root when the document misses', () => {
871
+ const host = document.createElement('div');
872
+ document.body.appendChild(host);
873
+ const shadow = host.attachShadow({ mode: 'open' });
874
+ const container = document.createElement('div');
875
+ container.id = 'shadow-target';
876
+ const reference = document.createElement('div');
877
+ shadow.appendChild(container);
878
+ shadow.appendChild(reference);
879
+
880
+ // document.querySelector cannot see into the shadow root.
881
+ expect(document.querySelector('#shadow-target')).toBeNull();
882
+ expect(querySelectorAcrossShadowRoot('#shadow-target', reference)).toBe(container);
883
+
884
+ host.remove();
885
+ });
886
+
887
+ it('should prefer the document match over the shadow root', () => {
888
+ const lightEl = document.createElement('div');
889
+ lightEl.id = 'shared-id';
890
+ document.body.appendChild(lightEl);
891
+
892
+ const host = document.createElement('div');
893
+ document.body.appendChild(host);
894
+ const shadow = host.attachShadow({ mode: 'open' });
895
+ const shadowEl = document.createElement('div');
896
+ shadowEl.id = 'shared-id';
897
+ const reference = document.createElement('div');
898
+ shadow.appendChild(shadowEl);
899
+ shadow.appendChild(reference);
900
+
901
+ expect(querySelectorAcrossShadowRoot('#shared-id', reference)).toBe(lightEl);
902
+
903
+ lightEl.remove();
904
+ host.remove();
905
+ });
906
+
907
+ it('should return null when nothing matches and the reference node is in the light DOM', () => {
908
+ const reference = document.createElement('div');
909
+ document.body.appendChild(reference);
910
+
911
+ expect(querySelectorAcrossShadowRoot('#does-not-exist', reference)).toBeNull();
912
+
913
+ reference.remove();
914
+ });
915
+
916
+ it('should return null when no reference node is provided and the document misses', () => {
917
+ expect(querySelectorAcrossShadowRoot('#does-not-exist')).toBeNull();
918
+ });
919
+ });
920
+
858
921
  describe('getEffectiveClippingAncestors', () => {
859
922
  it('should return an empty list when element is missing', () => {
860
923
  expect(getEffectiveClippingAncestors(null as unknown as HTMLElement)).toEqual([]);
package/src/utils.ts CHANGED
@@ -291,6 +291,30 @@ export function getDOMNode(domSelector: DOMSelector): HTMLElement {
291
291
  return typeof domSelector === 'string' ? document.querySelector(domSelector) : domSelector;
292
292
  }
293
293
 
294
+ /**
295
+ * Resolves a CSS selector to an element. `document.querySelector` cannot pierce
296
+ * shadow boundaries, so when the document lookup misses and a reference node is
297
+ * provided, this also searches the shadow root the reference node lives in —
298
+ * letting a selector target an element inside the same shadow root as the embed
299
+ * instead of silently falling back to the light DOM.
300
+ * @param selector CSS selector string.
301
+ * @param referenceNode Node whose shadow root is searched when the document lookup misses.
302
+ */
303
+ export function querySelectorAcrossShadowRoot(
304
+ selector: string,
305
+ referenceNode?: Node | null,
306
+ ): Element | null {
307
+ const fromDocument = document.querySelector(selector);
308
+ if (fromDocument) {
309
+ return fromDocument;
310
+ }
311
+ const rootNode = referenceNode?.getRootNode?.();
312
+ if (typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot) {
313
+ return rootNode.querySelector(selector);
314
+ }
315
+ return null;
316
+ }
317
+
294
318
  export const deepMerge = (target: any, source: any) => merge(target, source);
295
319
 
296
320
  export const getOperationNameFromQuery = (query: string) => {