@thoughtspot/visual-embed-sdk 1.53.0 → 1.53.1

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 (151) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/contracts/contracts.spec.js +4 -0
  3. package/cjs/src/contracts/contracts.spec.js.map +1 -1
  4. package/cjs/src/contracts/host-event-contracts.d.ts +10 -0
  5. package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
  6. package/cjs/src/embed/app.d.ts +1 -0
  7. package/cjs/src/embed/app.d.ts.map +1 -1
  8. package/cjs/src/embed/app.js.map +1 -1
  9. package/cjs/src/embed/conversation.d.ts +14 -0
  10. package/cjs/src/embed/conversation.d.ts.map +1 -1
  11. package/cjs/src/embed/conversation.js.map +1 -1
  12. package/cjs/src/embed/conversation.spec.js +22 -0
  13. package/cjs/src/embed/conversation.spec.js.map +1 -1
  14. package/cjs/src/embed/host-events.spec.js +32 -0
  15. package/cjs/src/embed/host-events.spec.js.map +1 -1
  16. package/cjs/src/embed/liveboard.d.ts +2 -16
  17. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  18. package/cjs/src/embed/liveboard.js +29 -10
  19. package/cjs/src/embed/liveboard.js.map +1 -1
  20. package/cjs/src/embed/liveboard.spec.js +97 -0
  21. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  22. package/cjs/src/embed/spotter-utils.d.ts +1 -0
  23. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  24. package/cjs/src/embed/spotter-utils.js +7 -1
  25. package/cjs/src/embed/spotter-utils.js.map +1 -1
  26. package/cjs/src/embed/spotter-utils.spec.js +19 -0
  27. package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
  28. package/cjs/src/embed/ts-embed.d.ts +21 -16
  29. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  30. package/cjs/src/embed/ts-embed.js +77 -63
  31. package/cjs/src/embed/ts-embed.js.map +1 -1
  32. package/cjs/src/embed/ts-embed.spec.js +240 -7
  33. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  34. package/cjs/src/index.d.ts +2 -2
  35. package/cjs/src/index.d.ts.map +1 -1
  36. package/cjs/src/index.js.map +1 -1
  37. package/cjs/src/react/all-types-export.d.ts +1 -1
  38. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  39. package/cjs/src/react/all-types-export.js.map +1 -1
  40. package/cjs/src/types.d.ts +69 -2
  41. package/cjs/src/types.d.ts.map +1 -1
  42. package/cjs/src/types.js +67 -0
  43. package/cjs/src/types.js.map +1 -1
  44. package/cjs/src/utils/runtimeTelemetry.d.ts +11 -0
  45. package/cjs/src/utils/runtimeTelemetry.d.ts.map +1 -0
  46. package/cjs/src/utils/runtimeTelemetry.js +34 -0
  47. package/cjs/src/utils/runtimeTelemetry.js.map +1 -0
  48. package/cjs/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  49. package/cjs/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  50. package/cjs/src/utils/runtimeTelemetry.spec.js +124 -0
  51. package/cjs/src/utils/runtimeTelemetry.spec.js.map +1 -0
  52. package/dist/{index-CyDR8j_0.js → index-CrBnZBZv.js} +1 -1
  53. package/dist/src/contracts/host-event-contracts.d.ts +10 -0
  54. package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
  55. package/dist/src/embed/app.d.ts +1 -0
  56. package/dist/src/embed/app.d.ts.map +1 -1
  57. package/dist/src/embed/conversation.d.ts +14 -0
  58. package/dist/src/embed/conversation.d.ts.map +1 -1
  59. package/dist/src/embed/liveboard.d.ts +2 -16
  60. package/dist/src/embed/liveboard.d.ts.map +1 -1
  61. package/dist/src/embed/spotter-utils.d.ts +1 -0
  62. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  63. package/dist/src/embed/ts-embed.d.ts +21 -16
  64. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  65. package/dist/src/index.d.ts +2 -2
  66. package/dist/src/index.d.ts.map +1 -1
  67. package/dist/src/react/all-types-export.d.ts +1 -1
  68. package/dist/src/react/all-types-export.d.ts.map +1 -1
  69. package/dist/src/types.d.ts +69 -2
  70. package/dist/src/types.d.ts.map +1 -1
  71. package/dist/src/utils/runtimeTelemetry.d.ts +11 -0
  72. package/dist/src/utils/runtimeTelemetry.d.ts.map +1 -0
  73. package/dist/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  74. package/dist/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  75. package/dist/tsembed-react.es.js +247 -78
  76. package/dist/tsembed-react.js +246 -77
  77. package/dist/tsembed.es.js +247 -78
  78. package/dist/tsembed.js +246 -77
  79. package/dist/visual-embed-sdk-react-full.d.ts +116 -18
  80. package/dist/visual-embed-sdk-react.d.ts +116 -18
  81. package/dist/visual-embed-sdk.d.ts +116 -33
  82. package/lib/package.json +1 -1
  83. package/lib/src/contracts/contracts.spec.js +4 -0
  84. package/lib/src/contracts/contracts.spec.js.map +1 -1
  85. package/lib/src/contracts/host-event-contracts.d.ts +10 -0
  86. package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
  87. package/lib/src/embed/app.d.ts +1 -0
  88. package/lib/src/embed/app.d.ts.map +1 -1
  89. package/lib/src/embed/app.js.map +1 -1
  90. package/lib/src/embed/conversation.d.ts +14 -0
  91. package/lib/src/embed/conversation.d.ts.map +1 -1
  92. package/lib/src/embed/conversation.js.map +1 -1
  93. package/lib/src/embed/conversation.spec.js +22 -0
  94. package/lib/src/embed/conversation.spec.js.map +1 -1
  95. package/lib/src/embed/host-events.spec.js +32 -0
  96. package/lib/src/embed/host-events.spec.js.map +1 -1
  97. package/lib/src/embed/liveboard.d.ts +2 -16
  98. package/lib/src/embed/liveboard.d.ts.map +1 -1
  99. package/lib/src/embed/liveboard.js +28 -10
  100. package/lib/src/embed/liveboard.js.map +1 -1
  101. package/lib/src/embed/liveboard.spec.js +97 -0
  102. package/lib/src/embed/liveboard.spec.js.map +1 -1
  103. package/lib/src/embed/spotter-utils.d.ts +1 -0
  104. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  105. package/lib/src/embed/spotter-utils.js +7 -1
  106. package/lib/src/embed/spotter-utils.js.map +1 -1
  107. package/lib/src/embed/spotter-utils.spec.js +19 -0
  108. package/lib/src/embed/spotter-utils.spec.js.map +1 -1
  109. package/lib/src/embed/ts-embed.d.ts +21 -16
  110. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  111. package/lib/src/embed/ts-embed.js +78 -64
  112. package/lib/src/embed/ts-embed.js.map +1 -1
  113. package/lib/src/embed/ts-embed.spec.js +240 -7
  114. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  115. package/lib/src/index.d.ts +2 -2
  116. package/lib/src/index.d.ts.map +1 -1
  117. package/lib/src/index.js.map +1 -1
  118. package/lib/src/react/all-types-export.d.ts +1 -1
  119. package/lib/src/react/all-types-export.d.ts.map +1 -1
  120. package/lib/src/react/all-types-export.js.map +1 -1
  121. package/lib/src/types.d.ts +69 -2
  122. package/lib/src/types.d.ts.map +1 -1
  123. package/lib/src/types.js +67 -0
  124. package/lib/src/types.js.map +1 -1
  125. package/lib/src/utils/runtimeTelemetry.d.ts +11 -0
  126. package/lib/src/utils/runtimeTelemetry.d.ts.map +1 -0
  127. package/lib/src/utils/runtimeTelemetry.js +28 -0
  128. package/lib/src/utils/runtimeTelemetry.js.map +1 -0
  129. package/lib/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  130. package/lib/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  131. package/lib/src/utils/runtimeTelemetry.spec.js +122 -0
  132. package/lib/src/utils/runtimeTelemetry.spec.js.map +1 -0
  133. package/lib/src/visual-embed-sdk.d.ts +116 -33
  134. package/package.json +1 -1
  135. package/src/contracts/contracts.spec.ts +4 -0
  136. package/src/contracts/host-event-contracts.ts +13 -0
  137. package/src/embed/app.ts +1 -0
  138. package/src/embed/conversation.spec.ts +24 -0
  139. package/src/embed/conversation.ts +14 -0
  140. package/src/embed/host-events.spec.ts +44 -0
  141. package/src/embed/liveboard.spec.ts +111 -0
  142. package/src/embed/liveboard.ts +34 -10
  143. package/src/embed/spotter-utils.spec.ts +20 -0
  144. package/src/embed/spotter-utils.ts +12 -1
  145. package/src/embed/ts-embed.spec.ts +297 -8
  146. package/src/embed/ts-embed.ts +79 -64
  147. package/src/index.ts +2 -0
  148. package/src/react/all-types-export.ts +1 -0
  149. package/src/types.ts +68 -0
  150. package/src/utils/runtimeTelemetry.spec.ts +148 -0
  151. package/src/utils/runtimeTelemetry.ts +50 -0
@@ -795,6 +795,30 @@ describe('SpotterEmbed APP_INIT embedParams', () => {
795
795
  ).toEqual(spotterChatPinConfig);
796
796
  });
797
797
 
798
+ it('should send enableConnectorMarketplace in embedParams.spotterSidebarConfig, not the URL', async () => {
799
+ const response = await getAppInitResponse({
800
+ worksheetId: 'ws1',
801
+ spotterSidebarConfig: {
802
+ enableConnectorMarketplace: true,
803
+ },
804
+ });
805
+ expect(response.data.embedParams.spotterSidebarConfig.enableConnectorMarketplace).toBe(true);
806
+ expect(getIFrameSrc()).not.toContain('enableConnectorMarketplace');
807
+ });
808
+
809
+ it('should send enableSpotterInstructionAndMoreSettings at the top level of embedParams', async () => {
810
+ const response = await getAppInitResponse({
811
+ worksheetId: 'ws1',
812
+ spotterSidebarConfig: {
813
+ enablePastConversationsSidebar: true,
814
+ enableSpotterInstructionAndMoreSettings: true,
815
+ },
816
+ });
817
+ expect(response.data.embedParams.enableSpotterInstructionAndMoreSettings).toBe(true);
818
+ expect(response.data.embedParams.spotterSidebarConfig)
819
+ .not.toHaveProperty('enableSpotterInstructionAndMoreSettings');
820
+ });
821
+
798
822
  it('should populate enablePastConversationsSidebar from deprecated standalone flag', async () => {
799
823
  const response = await getAppInitResponse({
800
824
  worksheetId: 'ws1',
@@ -152,6 +152,19 @@ export interface SpotterSidebarViewConfig {
152
152
  * @default Analysts
153
153
  */
154
154
  spotterAnalystsLabel?: string;
155
+ /**
156
+ * Shows the connector marketplace option in the settings menu of the
157
+ * Spotter sidebar footer. When not set, the cluster setting applies.
158
+ * @version SDK: 1.53.0 | ThoughtSpot Cloud: 26.10.0.cl
159
+ */
160
+ enableConnectorMarketplace?: boolean;
161
+ /**
162
+ * Shows the Spotter instructions and more settings options in the
163
+ * settings menu of the Spotter sidebar footer.
164
+ * @version SDK: 1.53.0 | ThoughtSpot Cloud: 26.10.0.cl
165
+ * @default false
166
+ */
167
+ enableSpotterInstructionAndMoreSettings?: boolean;
155
168
  }
156
169
 
157
170
  /**
@@ -822,6 +835,7 @@ export interface SpotterAppInitData extends DefaultAppInitData {
822
835
  visualOverridesParams?: VisualizationOverrides | null;
823
836
  starterPrompts?: StarterPromptsConfig;
824
837
  spotterAnalystConfig?: SpotterAnalystConfig;
838
+ enableSpotterInstructionAndMoreSettings?: boolean;
825
839
  };
826
840
  }
827
841
 
@@ -636,6 +636,50 @@ describe('HostEvent.Edit', () => {
636
636
  });
637
637
  });
638
638
 
639
+ // ---------------------------------------------------------------------------
640
+ // ExitEditMode
641
+ // ---------------------------------------------------------------------------
642
+ describe('HostEvent.ExitEditMode', () => {
643
+ test("enum value is 'exitEditMode' (wire contract with the host app)", () => {
644
+ expect(HostEvent.ExitEditMode).toBe('exitEditMode');
645
+ });
646
+
647
+ test("postMessage type is 'exitEditMode' (camelCase)", async () => {
648
+ mockMessageChannel();
649
+ const { lb, iframe } = await renderLiveboard();
650
+ await executeAfterWait(() => {
651
+ lb.trigger(HostEvent.ExitEditMode);
652
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
653
+ expect.objectContaining({ type: 'exitEditMode' }),
654
+ thoughtSpotHost,
655
+ expect.anything(),
656
+ );
657
+ });
658
+ });
659
+
660
+ test('viewConfig.vizId is injected into the payload as data.vizId', async () => {
661
+ mockMessageChannel();
662
+ const { lb, iframe } = await renderLiveboard({ vizId });
663
+ await executeAfterWait(() => {
664
+ lb.trigger(HostEvent.ExitEditMode, {} as any);
665
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
666
+ expect.objectContaining({
667
+ type: 'exitEditMode',
668
+ data: expect.objectContaining({ vizId }),
669
+ }),
670
+ thoughtSpotHost,
671
+ expect.anything(),
672
+ );
673
+ });
674
+ });
675
+
676
+ test('returns null when !isRendered', async () => {
677
+ const lb = unrenderedLiveboard();
678
+ const result = await lb.trigger(HostEvent.ExitEditMode);
679
+ expect(result).toBeNull();
680
+ });
681
+ });
682
+
639
683
  // ---------------------------------------------------------------------------
640
684
  // 30 – CopyLink (maps to 'embedDocument')
641
685
  // ---------------------------------------------------------------------------
@@ -158,6 +158,25 @@ describe('Liveboard/viz embed tests', () => {
158
158
  });
159
159
  });
160
160
 
161
+ test('should hide and disable the Cancel button in the Liveboard edit header', async () => {
162
+ expect(Action.Cancel).toBe('cancel');
163
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
164
+ hiddenActions: [Action.Cancel],
165
+ disabledActions: [Action.Cancel],
166
+ disabledActionReason: 'Save your changes to leave edit mode',
167
+ ...defaultViewConfig,
168
+ liveboardId,
169
+ } as LiveboardViewConfig);
170
+ liveboardEmbed.render();
171
+ await executeAfterWait(() => {
172
+ expectUrlToHaveParamsWithValues(getIFrameSrc(), {
173
+ hideAction: JSON.stringify([Action.ReportError, Action.Cancel]),
174
+ disableAction: JSON.stringify([Action.Cancel]),
175
+ disableHint: 'Save your changes to leave edit mode',
176
+ });
177
+ });
178
+ });
179
+
161
180
  test('should hide and disable the Spotter button on the Liveboard header', async () => {
162
181
  expect(Action.SpotterOnLiveboard).toBe('spotterOnLiveboard');
163
182
  const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
@@ -2832,6 +2851,98 @@ describe('Liveboard/viz embed tests', () => {
2832
2851
  expect(navigateToLiveboardSpy).toHaveBeenCalled();
2833
2852
  });
2834
2853
 
2854
+ test('should not route via home when a new instance shows an equal config', async () => {
2855
+ const viewConfig = {
2856
+ liveboardId,
2857
+ vizId,
2858
+ activeTabId,
2859
+ ...defaultViewConfig,
2860
+ };
2861
+ const previous = new LiveboardEmbed(getRootEl(), { ...viewConfig, beforeRender: () => {} } as any);
2862
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), { ...viewConfig, beforeRender: () => {} } as any);
2863
+
2864
+ // React constructs a new embed on every mount.
2865
+ jest.spyOn(liveboardEmbed as any, 'getPreRenderObj').mockReturnValue(previous);
2866
+ const triggerSpy = jest
2867
+ .spyOn(liveboardEmbed, 'trigger')
2868
+ .mockImplementation(() => Promise.resolve(undefined as any));
2869
+ const navigateToLiveboardSpy = jest
2870
+ .spyOn(liveboardEmbed, 'navigateToLiveboard')
2871
+ .mockImplementation(() => Promise.resolve(undefined));
2872
+
2873
+ liveboardEmbed.isEmbedContainerLoaded = false;
2874
+ liveboardEmbed['beforePrerenderVisible']();
2875
+ liveboardEmbed.isEmbedContainerLoaded = true;
2876
+ liveboardEmbed['executeEmbedContainerReadyCallbacks']();
2877
+ await waitForPreRenderNavigate();
2878
+
2879
+ expect(triggerSpy).not.toHaveBeenCalledWith(HostEvent.Navigate, 'home');
2880
+ expect(navigateToLiveboardSpy).toHaveBeenCalled();
2881
+ });
2882
+
2883
+ test('should not route via home when the previous instance has been shown once', async () => {
2884
+ // navigateToLiveboard assigns vizId/activeTabId/personalizedViewId
2885
+ // unconditionally, so a shown instance carries them as undefined
2886
+ // while a fresh one lacks the keys entirely. Comparing raw objects
2887
+ // counts own keys, so it never matches again.
2888
+ const viewConfig = { liveboardId, ...defaultViewConfig };
2889
+ const previous = new LiveboardEmbed(getRootEl(), { ...viewConfig } as any);
2890
+ (previous as any).viewConfig.vizId = undefined;
2891
+ (previous as any).viewConfig.activeTabId = undefined;
2892
+ (previous as any).viewConfig.personalizedViewId = undefined;
2893
+
2894
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), { ...viewConfig } as any);
2895
+
2896
+ jest.spyOn(liveboardEmbed as any, 'getPreRenderObj').mockReturnValue(previous);
2897
+ const triggerSpy = jest
2898
+ .spyOn(liveboardEmbed, 'trigger')
2899
+ .mockImplementation(() => Promise.resolve(undefined as any));
2900
+ jest
2901
+ .spyOn(liveboardEmbed, 'navigateToLiveboard')
2902
+ .mockImplementation(() => Promise.resolve(undefined));
2903
+
2904
+ liveboardEmbed.isEmbedContainerLoaded = false;
2905
+ liveboardEmbed['beforePrerenderVisible']();
2906
+ liveboardEmbed.isEmbedContainerLoaded = true;
2907
+ liveboardEmbed['executeEmbedContainerReadyCallbacks']();
2908
+ await waitForPreRenderNavigate();
2909
+
2910
+ expect(triggerSpy).not.toHaveBeenCalledWith(HostEvent.Navigate, 'home');
2911
+ });
2912
+
2913
+ test('should route via home when a new instance shows the same route with other filters', async () => {
2914
+ const viewConfig = {
2915
+ liveboardId,
2916
+ vizId,
2917
+ activeTabId,
2918
+ ...defaultViewConfig,
2919
+ };
2920
+ const previous = new LiveboardEmbed(getRootEl(), {
2921
+ ...viewConfig,
2922
+ runtimeFilters: [{ columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['east'] }],
2923
+ });
2924
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
2925
+ ...viewConfig,
2926
+ runtimeFilters: [{ columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] }],
2927
+ });
2928
+
2929
+ jest.spyOn(liveboardEmbed as any, 'getPreRenderObj').mockReturnValue(previous);
2930
+ const triggerSpy = jest
2931
+ .spyOn(liveboardEmbed, 'trigger')
2932
+ .mockImplementation(() => Promise.resolve(undefined as any));
2933
+ jest
2934
+ .spyOn(liveboardEmbed, 'navigateToLiveboard')
2935
+ .mockImplementation(() => Promise.resolve(undefined));
2936
+
2937
+ liveboardEmbed.isEmbedContainerLoaded = false;
2938
+ liveboardEmbed['beforePrerenderVisible']();
2939
+ liveboardEmbed.isEmbedContainerLoaded = true;
2940
+ liveboardEmbed['executeEmbedContainerReadyCallbacks']();
2941
+ await waitForHomeHopNavigate();
2942
+
2943
+ expect(triggerSpy).toHaveBeenCalledWith(HostEvent.Navigate, 'home');
2944
+ });
2945
+
2835
2946
  test('should not route via home when the pre-render is on another liveboard', async () => {
2836
2947
  const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
2837
2948
  liveboardId,
@@ -8,6 +8,7 @@
8
8
  * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
9
9
  */
10
10
 
11
+ import isEqualWith from 'lodash/isEqualWith';
11
12
  import { getPreview } from '../utils/graphql/preview-service';
12
13
  import { ERROR_MESSAGE } from '../errors';
13
14
  import {
@@ -605,6 +606,14 @@ export interface LiveboardViewConfig
605
606
  * })
606
607
  * ```
607
608
  */
609
+ /**
610
+ * A shallow copy without the keys whose value is `undefined`, so a key that was
611
+ * assigned `undefined` compares equal to one that was never set.
612
+ */
613
+ const definedEntries = (config: Record<string, any> = {}): Record<string, any> => Object.fromEntries(
614
+ Object.entries(config).filter(([, value]) => value !== undefined),
615
+ );
616
+
608
617
  export class LiveboardEmbed extends V1Embed {
609
618
  protected viewConfig: LiveboardViewConfig;
610
619
 
@@ -1004,17 +1013,19 @@ export class LiveboardEmbed extends V1Embed {
1004
1013
  super.beforePrerenderVisible();
1005
1014
 
1006
1015
  // Captured before showPreRender() hands the wrapper over to this
1007
- // instance. Itself means a hide/show, not a hand-over, so there is
1008
- // nothing stale to clear.
1016
+ // instance. React builds a new instance per mount, so an equal config
1017
+ // is a hide/show, not a hand-over: there is nothing stale to clear.
1009
1018
  const previous = this.getPreRenderObj<LiveboardEmbed>();
1010
- const showing = previous === this ? undefined : previous?.currentLiveboardState;
1019
+ const previousRoute = this.isSameEmbedConfig(previous)
1020
+ ? undefined
1021
+ : previous?.currentLiveboardState;
1011
1022
 
1012
1023
  this.executeAfterEmbedContainerLoaded(async () => {
1013
1024
  // Without this the params callback suspends on its await and
1014
1025
  // Navigate goes out first, so the container loads with the previous
1015
1026
  // config's filters.
1016
1027
  await this.preRenderParamsApplied;
1017
- if (this.isShowingLiveboardRoute(showing)) {
1028
+ if (this.isAlreadyOnSameRoute(previousRoute)) {
1018
1029
  // Navigate to the route we are already on is a no-op, so the
1019
1030
  // liveboard keeps its state. Home unmounts the container, which
1020
1031
  // clears it.
@@ -1041,18 +1052,31 @@ export class LiveboardEmbed extends V1Embed {
1041
1052
  });
1042
1053
  }
1043
1054
 
1055
+ // definedEntries drops undefined-valued keys: a shown instance carries
1056
+ // vizId/activeTabId/etc as undefined, so a raw isEqual never matches again.
1057
+ // Functions compare equal, since React passes a fresh closure per render.
1058
+ private isSameEmbedConfig(previous?: LiveboardEmbed): boolean {
1059
+ if (!previous) return false;
1060
+ if (previous === this) return true;
1061
+ return isEqualWith(
1062
+ definedEntries(previous.viewConfig),
1063
+ definedEntries(this.viewConfig),
1064
+ (a, b) => (typeof a === 'function' && typeof b === 'function' ? true : undefined),
1065
+ );
1066
+ }
1067
+
1044
1068
  // The whole route, not just the liveboard id: the path is built from the
1045
1069
  // viz, tab and view too, so a same-liveboard show onto another tab is a
1046
1070
  // real navigation.
1047
- private isShowingLiveboardRoute(showing?: LiveboardEmbed['currentLiveboardState']): boolean {
1048
- if (!showing?.liveboardId) return false;
1071
+ private isAlreadyOnSameRoute(previousRoute?: LiveboardEmbed['currentLiveboardState']): boolean {
1072
+ if (!previousRoute?.liveboardId) return false;
1049
1073
  const {
1050
1074
  liveboardId, vizId, activeTabId, personalizedViewId,
1051
1075
  } = this.viewConfig;
1052
- return showing.liveboardId === liveboardId
1053
- && showing.vizId === vizId
1054
- && showing.activeTabId === activeTabId
1055
- && showing.personalizedViewId === personalizedViewId;
1076
+ return previousRoute.liveboardId === liveboardId
1077
+ && previousRoute.vizId === vizId
1078
+ && previousRoute.activeTabId === activeTabId
1079
+ && previousRoute.personalizedViewId === personalizedViewId;
1056
1080
  }
1057
1081
 
1058
1082
  protected async handleRenderForPrerender(): Promise<TsEmbed> {
@@ -58,6 +58,26 @@ describe('buildSpotterSidebarAppInitData', () => {
58
58
  .toEqual(spotterChatPinConfig);
59
59
  });
60
60
 
61
+ it('lifts enableSpotterInstructionAndMoreSettings to the top level of embedParams', () => {
62
+ const result = buildSpotterSidebarAppInitData(base, {
63
+ spotterSidebarConfig: {
64
+ enablePastConversationsSidebar: true,
65
+ enableSpotterInstructionAndMoreSettings: false,
66
+ },
67
+ }, noopError);
68
+ expect(result.embedParams?.enableSpotterInstructionAndMoreSettings).toBe(false);
69
+ expect(result.embedParams?.spotterSidebarConfig).toEqual({
70
+ enablePastConversationsSidebar: true,
71
+ });
72
+ });
73
+
74
+ it('omits enableSpotterInstructionAndMoreSettings when it is not set', () => {
75
+ const result = buildSpotterSidebarAppInitData(base, {
76
+ spotterSidebarConfig: { enablePastConversationsSidebar: true },
77
+ }, noopError);
78
+ expect(result.embedParams).not.toHaveProperty('enableSpotterInstructionAndMoreSettings');
79
+ });
80
+
61
81
  it('promotes standalone flag into spotterSidebarConfig.enablePastConversationsSidebar', () => {
62
82
  const result = buildSpotterSidebarAppInitData(base, { enablePastConversationsSidebar: true }, noopError);
63
83
  expect(result.embedParams?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
@@ -36,6 +36,7 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
36
36
  embedParams?: {
37
37
  spotterSidebarConfig?: SpotterSidebarViewConfig;
38
38
  visualOverridesParams?: VisualizationOverrides | null;
39
+ enableSpotterInstructionAndMoreSettings?: boolean;
39
40
  };
40
41
  } {
41
42
  const { spotterSidebarConfig, enablePastConversationsSidebar, visualOverrides } = viewConfig;
@@ -56,8 +57,15 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
56
57
  };
57
58
  }
58
59
 
60
+ // The app reads enableSpotterInstructionAndMoreSettings from the top level
61
+ // of embedParams, so lift it out of the sidebar config.
62
+ const {
63
+ enableSpotterInstructionAndMoreSettings,
64
+ ...sidebarConfigRest
65
+ } = spotterSidebarConfig || {};
66
+
59
67
  const resolvedSidebarConfig: SpotterSidebarViewConfig = {
60
- ...spotterSidebarConfig,
68
+ ...sidebarConfigRest,
61
69
  ...(resolvedEnablePastConversations !== undefined && {
62
70
  enablePastConversationsSidebar: resolvedEnablePastConversations,
63
71
  }),
@@ -81,6 +89,9 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
81
89
  embedParams: {
82
90
  ...((defaultAppInitData as any).embedParams || {}),
83
91
  spotterSidebarConfig: resolvedSidebarConfig,
92
+ ...(enableSpotterInstructionAndMoreSettings !== undefined
93
+ ? { enableSpotterInstructionAndMoreSettings }
94
+ : {}),
84
95
  ...(visualOverrides !== undefined ? { visualOverridesParams: visualOverrides } : {}),
85
96
  },
86
97
  };