diff --git a/src/embed/app.ts b/src/embed/app.ts index 9093ab7b..6b21887c 100644 --- a/src/embed/app.ts +++ b/src/embed/app.ts @@ -19,6 +19,7 @@ import { AllEmbedViewConfig, } from '../types'; import { V1Embed } from './ts-embed'; +import { PageContextOptions } from './hostEventClient/contracts'; /** * Pages within the ThoughtSpot app that can be embedded. diff --git a/src/embed/conversation.ts b/src/embed/conversation.ts index 74532e55..9e5a4676 100644 --- a/src/embed/conversation.ts +++ b/src/embed/conversation.ts @@ -3,6 +3,7 @@ import { ERROR_MESSAGE } from '../errors'; import { Param, BaseViewConfig, RuntimeFilter, RuntimeParameter, ErrorDetailsTypes, EmbedErrorCodes } from '../types'; import { TsEmbed } from './ts-embed'; import { getQueryParamString, getFilterQuery, getRuntimeParameters } from '../utils'; +import { PageContextOptions } from './hostEventClient/contracts'; /** * Configuration for search options @@ -328,6 +329,16 @@ export class SpotterEmbed extends TsEmbed { await this.renderIFrame(src); return this; } + + /** + * Get the current context of the embedded SpotterEmbed. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + const context = await super.getCurrentContext(); + return context; + } } /** diff --git a/src/embed/hostEventClient/contracts.ts b/src/embed/hostEventClient/contracts.ts index 0f2e07f1..6911452e 100644 --- a/src/embed/hostEventClient/contracts.ts +++ b/src/embed/hostEventClient/contracts.ts @@ -103,7 +103,7 @@ export type HostEventRequest = ? UIPassthroughRequest : any; -export type HostEventResponse = +export type HostEventResponse = HostEventT extends keyof EmbedApiHostEventMapping ? UIPassthroughResponse : any; @@ -111,5 +111,31 @@ export type HostEventResponse = // trigger response and request export type TriggerPayload = PayloadT | HostEventRequest; -export type TriggerResponse = - PayloadT extends HostEventRequest ? HostEventResponse : any; +export type TriggerResponse = + PayloadT extends HostEventRequest ? HostEventResponse : any; + +export enum ContextType { + Search = 'search-answer', + Liveboard = 'liveboard', + Answer = 'answer', + Spotter = 'spotter', + Sage = 'sage', +} + +export enum PageType { + PAGE = 'page', + DIALOG = 'dialog', +} + +interface ObjectIds { + answerId?: string; + liveboardId?: string; + vizIds?: string[]; + dataModelIds?: string[]; +} + +export interface PageContextOptions { + page: ContextType; + pageType: PageType; + objectIds: ObjectIds; +} diff --git a/src/embed/hostEventClient/host-event-client.spec.ts b/src/embed/hostEventClient/host-event-client.spec.ts index 316342b6..1d4830b5 100644 --- a/src/embed/hostEventClient/host-event-client.spec.ts +++ b/src/embed/hostEventClient/host-event-client.spec.ts @@ -54,6 +54,7 @@ describe('HostEventClient', () => { type: apiName, parameters, }, + undefined, ); expect(result).toEqual(await triggerResponse); }); @@ -148,6 +149,7 @@ describe('HostEventClient', () => { HostEvent.UIPassthrough, 'http://localhost', { parameters: payload, type: UIPassthroughEvent.PinAnswerToLiveboard }, + undefined, ); expect(result).toEqual(mockResponse.value); }); @@ -185,6 +187,7 @@ describe('HostEventClient', () => { parameters: payload, type: 'saveAnswer', }, + undefined, ); expect(result).toEqual({ answerId: 'newAnswer', ...mockResponse[0].value }); }); @@ -198,7 +201,7 @@ describe('HostEventClient', () => { const result = await client.triggerHostEvent(hostEvent, payload); - expect(client.hostEventFallback).toHaveBeenCalledWith(hostEvent, payload); + expect(client.hostEventFallback).toHaveBeenCalledWith(hostEvent, payload, undefined); expect(result).toEqual(mockResponse); }); @@ -223,6 +226,7 @@ describe('HostEventClient', () => { HostEvent.Pin, mockThoughtSpotHost, {}, + undefined, ); expect(result).toEqual([mockResponse]); }); @@ -248,6 +252,7 @@ describe('HostEventClient', () => { HostEvent.Save, mockThoughtSpotHost, {}, + undefined, ); expect(result).toEqual([mockResponse]); }); @@ -303,6 +308,7 @@ describe('HostEventClient', () => { parameters: { ...payload, pinboardId: 'test', newPinboardName: 'testLiveboard' }, type: 'addVizToPinboard', }, + undefined, ); expect(result).toEqual({ pinboardId: 'testLiveboard', diff --git a/src/embed/hostEventClient/host-event-client.ts b/src/embed/hostEventClient/host-event-client.ts index a98812ea..6aa8301f 100644 --- a/src/embed/hostEventClient/host-event-client.ts +++ b/src/embed/hostEventClient/host-event-client.ts @@ -8,6 +8,7 @@ import { UIPassthroughResponse, TriggerPayload, TriggerResponse, + ContextType, } from './contracts'; export class HostEventClient { @@ -23,7 +24,7 @@ export class HostEventClient { * @param {any} data Data to send with the host event * @returns {Promise} - the response from the process trigger */ - protected async processTrigger(message: HostEvent, data: any): Promise { + protected async processTrigger(message: HostEvent, data: any, context?: ContextType): Promise { if (!this.iFrame) { throw new Error('Iframe element is not set'); } @@ -34,14 +35,16 @@ export class HostEventClient { message, thoughtspotHost, data, + context, ); } public async handleHostEventWithParam( apiName: UIPassthroughEventT, parameters: UIPassthroughRequest, + context?: ContextType, ): Promise> { - const response = (await this.triggerUIPassthroughApi(apiName, parameters)) + const response = (await this.triggerUIPassthroughApi(apiName, parameters, context as ContextType)) ?.filter?.((r) => r.error || r.value)[0]; if (!response) { @@ -65,8 +68,9 @@ export class HostEventClient { public async hostEventFallback( hostEvent: HostEvent, data: any, + context?: ContextType, ): Promise { - return this.processTrigger(hostEvent, data); + return this.processTrigger(hostEvent, data, context); } /** @@ -80,20 +84,22 @@ export class HostEventClient { public async triggerUIPassthroughApi( apiName: UIPassthroughEventT, parameters: UIPassthroughRequest, + context?: ContextType, ): Promise> { const res = await this.processTrigger(HostEvent.UIPassthrough, { type: apiName, parameters, - }); + }, context); return res; } protected async handlePinEvent( payload: HostEventRequest, - ): Promise> { + context?: ContextType, + ): Promise> { if (!payload || !('newVizName' in payload)) { - return this.hostEventFallback(HostEvent.Pin, payload); + return this.hostEventFallback(HostEvent.Pin, payload, context); } const formattedPayload = { @@ -104,6 +110,7 @@ export class HostEventClient { const data = await this.handleHostEventWithParam( UIPassthroughEvent.PinAnswerToLiveboard, formattedPayload, + context as ContextType, ); return { @@ -114,14 +121,16 @@ export class HostEventClient { protected async handleSaveAnswerEvent( payload: HostEventRequest, + context?: ContextType, ): Promise { if (!payload || !('name' in payload) || !('description' in payload)) { // Save is the fallback for SaveAnswer - return this.hostEventFallback(HostEvent.Save, payload); + return this.hostEventFallback(HostEvent.Save, payload, context); } const data = await this.handleHostEventWithParam( UIPassthroughEvent.SaveAnswer, payload, + context as ContextType, ); return { ...data, @@ -132,19 +141,22 @@ export class HostEventClient { public async triggerHostEvent< HostEventT extends HostEvent, PayloadT, + ContextT extends ContextType, >( hostEvent: HostEventT, payload?: TriggerPayload, - ): Promise> { + context?: ContextT, + ): Promise> { switch (hostEvent) { case HostEvent.Pin: - return this.handlePinEvent(payload as HostEventRequest) as any; + return this.handlePinEvent(payload as HostEventRequest, context as ContextType) as any; case HostEvent.SaveAnswer: return this.handleSaveAnswerEvent( payload as HostEventRequest, + context as ContextType, ) as any; default: - return this.hostEventFallback(hostEvent, payload); + return this.hostEventFallback(hostEvent, payload, context); } } } diff --git a/src/embed/liveboard.spec.ts b/src/embed/liveboard.spec.ts index 03f8115e..0f9f490b 100644 --- a/src/embed/liveboard.spec.ts +++ b/src/embed/liveboard.spec.ts @@ -1495,7 +1495,7 @@ describe('Liveboard/viz embed tests', () => { await liveboardEmbed.trigger(HostEvent.Save); expect(mockProcessTrigger).toHaveBeenCalledWith(HostEvent.Save, { vizId: 'testViz', - }); + }, undefined); }); }); }); diff --git a/src/embed/liveboard.ts b/src/embed/liveboard.ts index 72a7dd27..8f1c2ee5 100644 --- a/src/embed/liveboard.ts +++ b/src/embed/liveboard.ts @@ -27,7 +27,7 @@ import { calculateVisibleElementData, getQueryParamString, isUndefined, isValidC import { getAuthPromise } from './base'; import { TsEmbed, V1Embed } from './ts-embed'; import { addPreviewStylesIfNotPresent } from '../utils/global-styles'; -import { TriggerPayload, TriggerResponse } from './hostEventClient/contracts'; +import { ContextType, TriggerPayload, TriggerResponse, PageContextOptions } from './hostEventClient/contracts'; import { logger } from '../utils/logger'; @@ -800,10 +800,11 @@ export class LiveboardEmbed extends V1Embed { * @param {any} data The payload to send with the message * @returns A promise that resolves with the response from the embedded app */ - public trigger( + public trigger( messageType: HostEventT, data: TriggerPayload = ({} as any), - ): Promise> { + context?: ContextT, + ): Promise> { const dataWithVizId: any = data; if (messageType === HostEvent.SetActiveTab) { this.setActiveTab(data as { tabId: string }); @@ -812,7 +813,7 @@ export class LiveboardEmbed extends V1Embed { if (typeof dataWithVizId === 'object' && this.viewConfig.vizId) { dataWithVizId.vizId = this.viewConfig.vizId; } - return super.trigger(messageType, dataWithVizId); + return super.trigger(messageType, dataWithVizId, context); } /** * Destroys the ThoughtSpot embed, and remove any nodes from the DOM. @@ -889,6 +890,16 @@ export class LiveboardEmbed extends V1Embed { return url; } + + /** + * Get the current context of the embedded liveboard. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + const context = await super.getCurrentContext(); + return context; + } } /** diff --git a/src/embed/sage.ts b/src/embed/sage.ts index a8818ae7..24a4678b 100644 --- a/src/embed/sage.ts +++ b/src/embed/sage.ts @@ -6,6 +6,7 @@ * @author Mourya Balabhadra */ +import { PageContextOptions } from './hostEventClient/contracts'; import { DOMSelector, Param, BaseViewConfig, SearchLiveboardCommonViewConfig } from '../types'; import { getQueryParamString } from '../utils'; import { V1Embed } from './ts-embed'; @@ -229,4 +230,14 @@ export class SageEmbed extends V1Embed { return this; } + + /** + * Get the current context of the embedded SageEmbed. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + const context = await super.getCurrentContext(); + return context; + } } diff --git a/src/embed/search-bar.tsx b/src/embed/search-bar.tsx index bc5f0262..e3855164 100644 --- a/src/embed/search-bar.tsx +++ b/src/embed/search-bar.tsx @@ -2,6 +2,7 @@ import { SearchLiveboardCommonViewConfig, BaseViewConfig, DefaultAppInitData, Pa import { getQueryParamString } from '../utils'; import { TsEmbed } from './ts-embed'; import { SearchOptions } from './search'; +import { PageContextOptions } from './hostEventClient/contracts'; /** * @group Embed components @@ -198,4 +199,14 @@ export class SearchBarEmbed extends TsEmbed { const defaultAppInitData = await super.getAppInitData(); return { ...defaultAppInitData, ...this.getSearchInitData() }; } + + /** + * Get the current context of the embedded search bar. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + const context = await super.getCurrentContext(); + return context; + } } diff --git a/src/embed/search.ts b/src/embed/search.ts index e572b9ac..6bad26b4 100644 --- a/src/embed/search.ts +++ b/src/embed/search.ts @@ -27,6 +27,7 @@ import { getAuthPromise } from './base'; import { getReleaseVersion } from '../auth'; import { getEmbedConfig } from './embedConfig'; import { getInterceptInitData } from '../api-intercept'; +import { PageContextOptions } from './hostEventClient/contracts'; /** * Configuration for search options. @@ -523,4 +524,14 @@ export class SearchEmbed extends TsEmbed { }); return this; } + + /** + * Get the current context of the embedded search. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + const context = await super.getCurrentContext(); + return context; + } } diff --git a/src/embed/ts-embed.spec.ts b/src/embed/ts-embed.spec.ts index 4444017f..156a97af 100644 --- a/src/embed/ts-embed.spec.ts +++ b/src/embed/ts-embed.spec.ts @@ -204,6 +204,7 @@ describe('Unit test case for ts embed', () => { parameters: payload, type: UIPassthroughEvent.PinAnswerToLiveboard, }, + undefined, ); }); }); @@ -224,6 +225,7 @@ describe('Unit test case for ts embed', () => { HostEvent.Save, 'http://tshost', {}, + undefined, ); }); }); @@ -245,6 +247,7 @@ describe('Unit test case for ts embed', () => { HostEvent.Save, 'http://tshost', false, + undefined, ); }); }); @@ -1331,6 +1334,7 @@ describe('Unit test case for ts embed', () => { HostEvent.InfoSuccess, 'http://tshost', expect.objectContaining({ info: expect.any(Object) }), + undefined, ); }); }); @@ -1467,6 +1471,7 @@ describe('Unit test case for ts embed', () => { HostEvent.InfoSuccess, 'http://tshost', expect.objectContaining({ info: expect.any(Object) }), + undefined, ); }); }); @@ -1481,6 +1486,7 @@ describe('Unit test case for ts embed', () => { HostEvent.InfoSuccess, 'http://tshost', expect.objectContaining({ info: expect.any(Object) }), + undefined, ); }); }); @@ -1495,6 +1501,7 @@ describe('Unit test case for ts embed', () => { HostEvent.InfoSuccess, 'http://tshost', expect.objectContaining({ info: expect.any(Object) }), + undefined, ); }); }); @@ -1509,6 +1516,7 @@ describe('Unit test case for ts embed', () => { HostEvent.InfoSuccess, 'http://tshost', expect.objectContaining({ info: expect.any(Object) }), + undefined, ); }); }); @@ -4055,7 +4063,7 @@ describe('Destroy error handling', () => { }).not.toThrow(); expect(logSpy).toHaveBeenCalledWith('Error destroying TS Embed', expect.any(Error)); - logSpy.mockRestore(); + logSpy.mockReset(); }); }); @@ -4098,11 +4106,12 @@ describe('Fullscreen change handler behavior', () => { document.dispatchEvent(event); await executeAfterWait(() => { - expect(mockProcessTrigger).toHaveBeenCalledWith( + expect(mockProcessTrigger).toHaveBeenLastCalledWith( expect.any(Object), HostEvent.ExitPresentMode, expect.any(String), expect.any(Object), + undefined, ); }); }); @@ -4195,13 +4204,14 @@ describe('ShowPreRender with UpdateEmbedParams', () => { embed2.showPreRender(); await executeAfterWait(() => { - expect(mockProcessTrigger).toHaveBeenCalledWith( + expect(mockProcessTrigger).toHaveBeenLastCalledWith( expect.any(Object), HostEvent.UpdateEmbedParams, expect.any(String), expect.objectContaining({ liveboardId: 'updated-lb', }), + undefined, ); }); }); @@ -4230,7 +4240,7 @@ describe('ShowPreRender with UpdateEmbedParams', () => { embed2.showPreRender(); await executeAfterWait(() => { - expect(mockProcessTrigger).toHaveBeenCalledWith( + expect(mockProcessTrigger).toHaveBeenLastCalledWith( expect.any(Object), HostEvent.UpdateEmbedParams, expect.any(String), @@ -4250,6 +4260,7 @@ describe('ShowPreRender with UpdateEmbedParams', () => { }, ], }), + undefined, ); }); }); @@ -4282,7 +4293,7 @@ describe('ShowPreRender with UpdateEmbedParams', () => { embed2.showPreRender(); await executeAfterWait(() => { - expect(mockProcessTrigger).toHaveBeenCalledWith( + expect(mockProcessTrigger).toHaveBeenLastCalledWith( expect.any(Object), HostEvent.UpdateEmbedParams, expect.any(String), @@ -4297,6 +4308,7 @@ describe('ShowPreRender with UpdateEmbedParams', () => { }, ], }), + undefined, ); }); }); diff --git a/src/embed/ts-embed.ts b/src/embed/ts-embed.ts index 0a0208dc..8f4cd918 100644 --- a/src/embed/ts-embed.ts +++ b/src/embed/ts-embed.ts @@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; import { + ContextType, TriggerPayload, TriggerResponse, UIPassthroughArrayResponse, @@ -75,6 +76,7 @@ import { ERROR_MESSAGE } from '../errors'; import { getPreauthInfo } from '../utils/sessionInfoService'; import { HostEventClient } from './hostEventClient/host-event-client'; import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept'; +import { getHostEventsConfig } from '../utils'; const { version } = pkgInfo; @@ -480,6 +482,7 @@ export class TsEmbed { hiddenListColumns: this.viewConfig.hiddenListColumns || [], customActions: customActionsResult.actions, ...getInterceptInitData(this.viewConfig), + ...getHostEventsConfig(this.viewConfig), }; return baseInitData; @@ -1347,10 +1350,11 @@ export class TsEmbed { * @param {any} data The payload to send with the message * @returns A promise that resolves with the response from the embedded app */ - public async trigger( + public async trigger( messageType: HostEventT, data: TriggerPayload = {} as any, - ): Promise> { + context?: ContextT, + ): Promise> { uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`); if (!this.isRendered) { @@ -1383,7 +1387,7 @@ export class TsEmbed { } // send an empty object, this is needed for liveboard default handlers - return this.hostEventClient.triggerHostEvent(messageType, data); + return this.hostEventClient.triggerHostEvent(messageType, data, context); } /** @@ -1425,6 +1429,20 @@ export class TsEmbed { return this.render(); } + /** + * Get the current context of the embedded TS component. + * @returns The current context object containing the page type and object ids. + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + */ + public async getCurrentContext(): Promise { + return new Promise((resolve) => { + this.executeAfterEmbedContainerLoaded(async () => { + const context = await this.trigger(HostEvent.GetPageContext, {}); + resolve(context); + }); + }); + } + /** * Creates the preRender shell * @param showPreRenderByDefault - Show the preRender after render, hidden by default diff --git a/src/types.ts b/src/types.ts index 31c629e9..0a7dddb7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1120,6 +1120,34 @@ export interface BaseViewConfig extends ApiInterceptFlags { * ``` */ customActions?: CustomAction[]; + + /** + * Flag to bypass host events payload validation + * @default false + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + * @example + * ```js + * const embed = new AppEmbed('#tsEmbed', { + * ... // other embed view config + * shouldBypassPayloadValidation:true, + * }) + * ``` + */ + shouldBypassPayloadValidation?: boolean; + + /** + * Flag to use host events v2. This is used to enable the new host events v2 API. + * @default false + * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl + * @example + * ```js + * const embed = new AppEmbed('#tsEmbed', { + * ... // other embed view config + * useHostEventsV2:true, + * }) + * ``` + */ + useHostEventsV2?: boolean; } /** @@ -4542,6 +4570,16 @@ export enum HostEvent { * @version SDK: 1.45.0 | ThoughtSpot: 26.2.0.cl */ StartNewSpotterConversation = 'StartNewSpotterConversation', + + /** + * Get the current context of the embedded page. + * @example + * ```js + * const context = await liveboardEmbed.trigger(HostEvent.GetPageContext); + * ``` + * @version SDK: 1.45.0 | ThoughtSpot: 26.2.0.cl + */ + GetPageContext = 'GetPageContext', } /** diff --git a/src/utils.ts b/src/utils.ts index 7d176c48..42035c47 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,6 +15,7 @@ import { DOMSelector, RuntimeParameter, AllEmbedViewConfig, + BaseViewConfig, } from './types'; import { logger } from './utils/logger'; import { ERROR_MESSAGE } from './errors'; @@ -560,6 +561,13 @@ export const formatTemplate = (template: string, values: Record): s }); }; +export const getHostEventsConfig = (viewConfig: BaseViewConfig) => { + return { + shouldBypassPayloadValidation: viewConfig.shouldBypassPayloadValidation, + useHostEventsV2: viewConfig.useHostEventsV2, + }; +} + /** * Check if the window is undefined * If the window is undefined, it means the code is running in a SSR environment. diff --git a/src/utils/processTrigger.ts b/src/utils/processTrigger.ts index ae907dfa..d27b3acf 100644 --- a/src/utils/processTrigger.ts +++ b/src/utils/processTrigger.ts @@ -3,6 +3,7 @@ import { HostEvent, MessagePayload } from '../types'; import { logger } from '../utils/logger'; import { handlePresentEvent } from '../utils'; import { getEmbedConfig } from '../embed/embedConfig'; +import { ContextType } from 'src/embed/hostEventClient/contracts'; /** * Reloads the ThoughtSpot iframe. @@ -22,12 +23,13 @@ export const reload = (iFrame: HTMLIFrameElement) => { * @param message * @param message.type * @param message.data + * @param message.context * @param thoughtSpotHost * @param channel */ function postIframeMessage( iFrame: HTMLIFrameElement, - message: { type: HostEvent; data: any }, + message: { type: HostEvent; data: any, context?: any }, thoughtSpotHost: string, channel?: MessageChannel, ) { @@ -42,12 +44,14 @@ export const TRIGGER_TIMEOUT = 30000; * @param messageType * @param thoughtSpotHost * @param data + * @param context */ export function processTrigger( iFrame: HTMLIFrameElement, messageType: HostEvent, thoughtSpotHost: string, data: any, + context?: ContextType, ): Promise { return new Promise((res, rej) => { if (messageType === HostEvent.Reload) { @@ -83,6 +87,6 @@ export function processTrigger( res(new Error(ERROR_MESSAGE.TRIGGER_TIMED_OUT)); }, TRIGGER_TIMEOUT); - return postIframeMessage(iFrame, { type: messageType, data }, thoughtSpotHost, channel); + return postIframeMessage(iFrame, { type: messageType, data, context }, thoughtSpotHost, channel); }); }