Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 1 addition & 23 deletions src/embed/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
} from '../types';
import { V1Embed } from './ts-embed';
import { SpotterChatViewConfig, SpotterSidebarViewConfig } from './conversation';
import { buildSpotterSidebarAppInitData } from './spotter-utils';
import { SpotterVizConfig, buildSpotterVizAppInitData } from './spotter-viz-utils';
import { SpotterVizConfig } from './spotter-viz-utils';

/**
* Pages within the ThoughtSpot app that can be embedded.
Expand Down Expand Up @@ -614,7 +613,7 @@
isPNGInScheduledEmailsEnabled?: boolean;

/**
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page

Check warning on line 616 in src/embed/app.ts

View workflow job for this annotation

GitHub Actions / build

Comments may not exceed 90 characters
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
* This feature is GA from version 26.5.0.cl. It is disabled by default in embed deployments.
*
Expand Down Expand Up @@ -905,27 +904,6 @@
}
}

/**
* Extends the default APP_INIT payload with `embedParams.spotterSidebarConfig`
* so the conv-assist app can read sidebar configuration on initialisation.
*
* Precedence for `enablePastConversationsSidebar`:
* `spotterSidebarConfig.enablePastConversationsSidebar` wins over the
* deprecated top-level `enablePastConversationsSidebar` flag; if the former
* is absent the latter is used as a fallback.
*
* An invalid `spotterDocumentationUrl` triggers a validation error and is
* excluded from the payload rather than forwarded to the app.
*/
protected async getAppInitData(): Promise<AppEmbedAppInitData> {
const defaultAppInitData = await super.getAppInitData();
const sidebarInitData = buildSpotterSidebarAppInitData(
defaultAppInitData,
this.viewConfig,
this.handleError.bind(this),
);
return buildSpotterVizAppInitData(sidebarInitData, this.viewConfig);
}

/**
* Constructs a map of parameters to be passed on to the
Expand Down Expand Up @@ -1221,7 +1199,7 @@
this.iFrame,
this.viewConfig.enableScrollableContainerLazyLoading,
);
// this should be fired only if the lazyLoadingForFullHeight and fullHeight are true

Check warning on line 1202 in src/embed/app.ts

View workflow job for this annotation

GitHub Actions / build

Comments may not exceed 80 characters
if(this.viewConfig.lazyLoadingForFullHeight && this.viewConfig.fullHeight){
this.trigger(HostEvent.VisibleEmbedCoordinates, data);
}
Expand Down
17 changes: 0 additions & 17 deletions src/embed/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import isUndefined from 'lodash/isUndefined';
import { ERROR_MESSAGE } from '../errors';
import { Param, BaseViewConfig, RuntimeFilter, RuntimeParameter, ErrorDetailsTypes, EmbedErrorCodes, DefaultAppInitData, VisualizationOverrides, SpotterFileUploadFileTypes } from '../types';
import { TsEmbed } from './ts-embed';
import { buildSpotterSidebarAppInitData } from './spotter-utils';
import { getQueryParamString, getFilterQuery, getRuntimeParameters, setParamIfDefined } from '../utils';

/**
Expand Down Expand Up @@ -405,22 +404,6 @@ export class SpotterEmbed extends TsEmbed {
super(container, viewConfig);
}

/**
* Extends the default APP_INIT payload with `embedParams.spotterSidebarConfig`
* so the conv-assist app can read sidebar configuration on initialisation.
*
* Precedence for `enablePastConversationsSidebar`:
* `spotterSidebarConfig.enablePastConversationsSidebar` wins over the
* deprecated top-level `enablePastConversationsSidebar` flag; if the former
* is absent the latter is used as a fallback.
*
* An invalid `spotterDocumentationUrl` triggers a validation error and is
* excluded from the payload rather than forwarded to the app.
*/
protected async getAppInitData(): Promise<SpotterAppInitData> {
const defaultAppInitData = await super.getAppInitData();
return buildSpotterSidebarAppInitData(defaultAppInitData, this.viewConfig, this.handleError.bind(this));
}

protected getEmbedParamsObject() {
const {
Expand Down
103 changes: 103 additions & 0 deletions src/embed/embedParams-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { buildEmbedParamsPayload } from './embedParams-builder';
import { ErrorDetailsTypes, EmbedErrorCodes } from '../types';
import { ERROR_MESSAGE } from '../errors';

describe('buildEmbedParamsPayload', () => {
const noopError = jest.fn();

it('returns undefined when nothing applies', () => {
expect(buildEmbedParamsPayload({}, noopError)).toBeUndefined();
});

describe('spotterViz', () => {
it('maps spotterViz to spotterVizConfig', () => {
const spotterViz = { brandName: 'MyBrand', description: 'Desc', inputChatPlaceholder: 'Ask...' };
const result = buildEmbedParamsPayload({ spotterViz }, noopError);
expect(result?.spotterVizConfig).toEqual(spotterViz);
});

it('omits spotterVizConfig when spotterViz is absent', () => {
const result = buildEmbedParamsPayload({ visualOverrides: { chart: {} } }, noopError);
expect(result?.spotterVizConfig).toBeUndefined();
});
});

describe('visualOverrides', () => {
it('maps visualOverrides to visualOverridesParams', () => {
const visualOverrides = { chart: { legend: { show: true, position: 'bottom' as const } } };
const result = buildEmbedParamsPayload({ visualOverrides }, noopError);
expect(result?.visualOverridesParams).toEqual(visualOverrides);
});

it('omits visualOverridesParams when visualOverrides is absent', () => {
const result = buildEmbedParamsPayload({ spotterViz: { brandName: 'X' } }, noopError);
expect(result?.visualOverridesParams).toBeUndefined();
});
});

describe('spotterSidebarConfig', () => {
it('returns empty/undefined when no sidebar config or standalone flag', () => {
expect(buildEmbedParamsPayload({}, noopError)).toBeUndefined();
});

it('passes spotterSidebarConfig through', () => {
const result = buildEmbedParamsPayload({
spotterSidebarConfig: { enablePastConversationsSidebar: true, spotterSidebarTitle: 'Chats' },
}, noopError);
expect(result?.spotterSidebarConfig).toEqual({
enablePastConversationsSidebar: true,
spotterSidebarTitle: 'Chats',
});
});

it('promotes the standalone enablePastConversationsSidebar flag into spotterSidebarConfig', () => {
const result = buildEmbedParamsPayload({ enablePastConversationsSidebar: true }, noopError);
expect(result?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
});

it('lets spotterSidebarConfig value take precedence over the standalone flag', () => {
const result = buildEmbedParamsPayload({
enablePastConversationsSidebar: false,
spotterSidebarConfig: { enablePastConversationsSidebar: true },
}, noopError);
expect(result?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
});

it('calls handleError and strips spotterDocumentationUrl when invalid', () => {
const handleError = jest.fn();
const result = buildEmbedParamsPayload({
spotterSidebarConfig: { spotterDocumentationUrl: 'not-a-url' },
}, handleError);
expect(handleError).toHaveBeenCalledWith(expect.objectContaining({
errorType: ErrorDetailsTypes.VALIDATION_ERROR,
message: ERROR_MESSAGE.INVALID_SPOTTER_DOCUMENTATION_URL,
code: EmbedErrorCodes.INVALID_URL,
}));
expect(result?.spotterSidebarConfig?.spotterDocumentationUrl).toBeUndefined();
});

it('keeps a valid spotterDocumentationUrl', () => {
const handleError = jest.fn();
const result = buildEmbedParamsPayload({
spotterSidebarConfig: { spotterDocumentationUrl: 'https://docs.example.com' },
}, handleError);
expect(handleError).not.toHaveBeenCalled();
expect(result?.spotterSidebarConfig?.spotterDocumentationUrl).toBe('https://docs.example.com');
});
});

describe('merging multiple fields', () => {
it('merges sidebar, viz and visualOverrides into one payload', () => {
const visualOverrides = { table: { display: { tableTheme: 'ZEBRA' } } };
const spotterViz = { brandName: 'MyBrand' };
const result = buildEmbedParamsPayload({
spotterSidebarConfig: { enablePastConversationsSidebar: true },
spotterViz,
visualOverrides,
}, noopError);
expect(result?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result?.spotterVizConfig).toEqual(spotterViz);
expect(result?.visualOverridesParams).toEqual(visualOverrides);
});
});
});
135 changes: 135 additions & 0 deletions src/embed/embedParams-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Declarative viewConfig -> embedParams mapping.
*
* This is the single place that knows which viewConfig fields become which
* embedParams keys in the APP_INIT payload. Each field is one row in
* EMBED_PARAM_FIELDS: trivial fields are pass-throughs; a field that needs
* transformation/validation gets its own resolver. Adding a new embedParams
* field is a one-row change here - the base class and embed subclasses are
* untouched.
*
* The base class (TsEmbed.getDefaultAppInitData) calls buildEmbedParamsPayload()
* exactly once for every embed type.
*/

import { ErrorDetailsTypes, EmbedErrorCodes } from '../types';
import type { VisualizationOverrides } from '../types';
import { validateHttpUrl } from '../utils';
import { ERROR_MESSAGE } from '../errors';
import type { SpotterVizConfig } from './spotter-viz-utils';
import type { SpotterSidebarViewConfig } from './conversation';
import { resolveEnablePastConversationsSidebar } from './spotter-utils';

/**
* The shape of the `embedParams` object sent inside APP_INIT. These keys are a
* contract with the consuming application - rename with care.
*/
export interface EmbedParamsPayload {
spotterSidebarConfig?: SpotterSidebarViewConfig;
spotterVizConfig?: SpotterVizConfig;
visualOverridesParams?: VisualizationOverrides | null;
}

/**
* The viewConfig fields that feed embedParams. All optional, so any embed's
* viewConfig is structurally assignable here (one cast at the base-class
* boundary covers the fact that the base ViewConfig type omits these).
*/
export interface EmbedParamsSourceConfig {
spotterSidebarConfig?: SpotterSidebarViewConfig;
enablePastConversationsSidebar?: boolean;
visualOverrides?: VisualizationOverrides;
spotterViz?: SpotterVizConfig;
}

/**
* Pulls a value for one embedParams field out of the viewConfig. Returning
* `undefined` omits the field from the payload.
*/
type EmbedParamResolver = (
viewConfig: EmbedParamsSourceConfig,
handleError: (err: any) => void,
) => unknown;

/**
* Pass-through resolver: emit viewConfig[from] unchanged. `null` is treated as
* "not set" so the field is omitted rather than sent as null.
*/
const passthrough = (from: keyof EmbedParamsSourceConfig): EmbedParamResolver =>
(viewConfig) => viewConfig[from] ?? undefined;

/**
* Resolver for spotterSidebarConfig - the one field with real logic. It folds
* the legacy standalone `enablePastConversationsSidebar` flag into the config
* and validates `spotterDocumentationUrl`, stripping it and reporting an error
* when the URL is invalid. Returns `undefined` when no sidebar config applies.
*/
const resolveSpotterSidebarConfig: EmbedParamResolver = (viewConfig, handleError) => {
const { spotterSidebarConfig, enablePastConversationsSidebar } = viewConfig;

const resolvedEnablePastConversations = resolveEnablePastConversationsSidebar({
spotterSidebarConfigValue: spotterSidebarConfig?.enablePastConversationsSidebar,
standaloneValue: enablePastConversationsSidebar,
});

if (!spotterSidebarConfig && resolvedEnablePastConversations === undefined) {
return undefined;
}

const resolved: SpotterSidebarViewConfig = {
...spotterSidebarConfig,
...(resolvedEnablePastConversations !== undefined && {
enablePastConversationsSidebar: resolvedEnablePastConversations,
}),
};

if (resolved.spotterDocumentationUrl !== undefined) {
const [isValid, validationError] = validateHttpUrl(resolved.spotterDocumentationUrl);
if (!isValid) {
handleError({
errorType: ErrorDetailsTypes.VALIDATION_ERROR,
message: ERROR_MESSAGE.INVALID_SPOTTER_DOCUMENTATION_URL,
code: EmbedErrorCodes.INVALID_URL,
error: validationError?.message || ERROR_MESSAGE.INVALID_SPOTTER_DOCUMENTATION_URL,
});
delete resolved.spotterDocumentationUrl;
}
}

return resolved;
};

/**
* The declarative table: one row per embedParams field. `key` is the output
* key in the payload; `resolve` produces its value from the viewConfig. Add a
* field by adding a row - nothing else changes.
*/
const EMBED_PARAM_FIELDS: ReadonlyArray<{
key: keyof EmbedParamsPayload;
resolve: EmbedParamResolver;
}> = [
{ key: 'spotterVizConfig', resolve: passthrough('spotterViz') },
{ key: 'visualOverridesParams', resolve: passthrough('visualOverrides') },
{ key: 'spotterSidebarConfig', resolve: resolveSpotterSidebarConfig },
];

/**
* Builds the embedParams payload for a viewConfig by running every field
* resolver and keeping the ones that produced a value. Returns `undefined` when
* nothing applies, so embedParams is omitted from APP_INIT entirely.
*/
export function buildEmbedParamsPayload(
viewConfig: EmbedParamsSourceConfig,
handleError: (err: any) => void,
): EmbedParamsPayload | undefined {
const payload: EmbedParamsPayload = {};

for (const { key, resolve } of EMBED_PARAM_FIELDS) {
const value = resolve(viewConfig, handleError);
if (value !== undefined) {
payload[key] = value as never;
}
}

return Object.keys(payload).length > 0 ? payload : undefined;
}
6 changes: 1 addition & 5 deletions src/embed/liveboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { addPreviewStylesIfNotPresent } from '../utils/global-styles';
import { TriggerPayload, TriggerResponse } from './hostEventClient/contracts';
import { logger } from '../utils/logger';
import { SpotterChatViewConfig } from './conversation';
import { SpotterVizConfig, buildSpotterVizAppInitData } from './spotter-viz-utils';
import { SpotterVizConfig } from './spotter-viz-utils';

/**
* APP_INIT data shape for LiveboardEmbed.
Expand Down Expand Up @@ -635,10 +635,6 @@ export class LiveboardEmbed extends V1Embed {
}
}

protected async getAppInitData(): Promise<LiveboardEmbedAppInitData> {
const defaultAppInitData = await super.getAppInitData();
return buildSpotterVizAppInitData(defaultAppInitData, this.viewConfig);
}

/**
* Construct a map of params to be passed on to the
Expand Down
11 changes: 1 addition & 10 deletions src/embed/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,19 +405,10 @@ export class SearchEmbed extends TsEmbed {

protected async getAppInitData(): Promise<SearchAppInitData> {
const defaultAppInitData = await super.getAppInitData();
const result: SearchAppInitData = {
return {
...defaultAppInitData,
...this.getSearchInitData(),
};

if (this.viewConfig.visualOverrides) {
result.embedParams = {
...((defaultAppInitData as any).embedParams || {}),
visualOverridesParams: this.viewConfig.visualOverrides,
};
}

return result;
}

protected getEmbedParamsObject() {
Expand Down
Loading
Loading