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
55 changes: 55 additions & 0 deletions src/embed/embedParams-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Declarative embedParams builder system.
*
* Owns the single place that knows which viewConfig properties map to which
* embedParams payloads. Embed subclasses do not build embedParams themselves;
* the base class calls collectEmbedParamsPayloads() once.
*/

import type { SpotterVizConfig } from './spotter-viz-utils';
import { buildSpotterVizEmbedParamsContribution } from './spotter-viz-utils';
import type { SpotterSidebarViewConfig } from './conversation';
import { buildSpotterSidebarEmbedParamsContribution } from './spotter-utils';
import type { VisualizationOverrides } from '../types';

export interface EmbedParamsContribution {
spotterSidebarConfig?: SpotterSidebarViewConfig;
spotterVizConfig?: SpotterVizConfig;
visualOverridesParams?: VisualizationOverrides | null;
}

/**
* The viewConfig properties that feed embedParams. All optional so any
* embed's viewConfig is structurally assignable without casts.
*/
export interface EmbedParamsSourceConfig {
spotterSidebarConfig?: SpotterSidebarViewConfig;
enablePastConversationsSidebar?: boolean;
visualOverrides?: VisualizationOverrides;
spotterViz?: SpotterVizConfig;
}

export function buildEmbedParams(
contributions: Partial<EmbedParamsContribution>[]
): Partial<EmbedParamsContribution> | undefined {
const merged = contributions.reduce((acc, contrib) => {
if (!contrib || Object.keys(contrib).length === 0) return acc;
return { ...acc, ...contrib };
}, {} as Partial<EmbedParamsContribution>);

return Object.keys(merged).length === 0 ? undefined : merged;
}

/**
* Detects which embedParams payloads apply for the given viewConfig and
* merges them. Returns undefined when nothing applies.
*/
export function collectEmbedParamsPayloads(
viewConfig: EmbedParamsSourceConfig,
handleError: (err: any) => void,
): Partial<EmbedParamsContribution> | undefined {
return buildEmbedParams([
buildSpotterSidebarEmbedParamsContribution(viewConfig, handleError),
buildSpotterVizEmbedParamsContribution(viewConfig),
]);
}
Comment on lines +32 to +55
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If all contributions are empty, buildEmbedParams currently returns an empty object {}. Since {} is truthy, this results in an empty embedParams object being added to the payload (e.g., embedParams: {}), which unnecessarily bloats the payload. Returning undefined when the merged object is empty prevents this.

Suggested change
export function buildEmbedParams(
contributions: Partial<EmbedParamsContribution>[]
): Partial<EmbedParamsContribution> | undefined {
if (contributions.length === 0) return undefined;
return contributions.reduce((acc, contrib) => {
if (!contrib || Object.keys(contrib).length === 0) return acc;
return { ...acc, ...contrib };
}, {} as Partial<EmbedParamsContribution>);
}
export function buildEmbedParams(
contributions: Partial<EmbedParamsContribution>[]
): Partial<EmbedParamsContribution> | undefined {
const merged = contributions.reduce((acc, contrib) => {
if (!contrib || Object.keys(contrib).length === 0) return acc;
return { ...acc, ...contrib };
}, {} as Partial<EmbedParamsContribution>);
return Object.keys(merged).length > 0 ? merged : undefined;
}
References
  1. Prefer omitting empty or unset parameters entirely from the payload/URL to avoid bloating. Returning undefined when the merged embedParams object is empty ensures it is omitted. (link)
  2. When a utility function processes optional configuration and its output is forwarded without immediate narrowing, it is acceptable to keep the return type optional to match the input's optionality, especially if a tighter type wouldn't benefit downstream consumers.

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
52 changes: 24 additions & 28 deletions src/embed/spotter-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resolveEnablePastConversationsSidebar, buildSpotterSidebarAppInitData } from './spotter-utils';
import { resolveEnablePastConversationsSidebar, buildSpotterSidebarEmbedParamsContribution } from './spotter-utils';
import { ErrorDetailsTypes, EmbedErrorCodes } from '../types';
import { ERROR_MESSAGE } from '../errors';

Expand All @@ -17,56 +17,52 @@ describe('resolveEnablePastConversationsSidebar', () => {
});
});

describe('buildSpotterSidebarAppInitData', () => {
const base = { type: 'APP_INIT' } as any;
describe('buildSpotterSidebarEmbedParamsContribution', () => {
const noopError = jest.fn();

it('returns base unchanged when no sidebar config or standalone flag', () => {
const result = buildSpotterSidebarAppInitData(base, {}, noopError);
expect(result).toBe(base);
it('returns empty object when no sidebar config or standalone flag', () => {
const result = buildSpotterSidebarEmbedParamsContribution({}, noopError);
expect(result).toEqual({});
});

it('nests spotterSidebarConfig under embedParams', () => {
const result = buildSpotterSidebarAppInitData(base, {
it('returns spotterSidebarConfig contribution', () => {
const result = buildSpotterSidebarEmbedParamsContribution({
spotterSidebarConfig: { enablePastConversationsSidebar: true, spotterSidebarTitle: 'Chats' },
}, noopError);
expect(result.embedParams?.spotterSidebarConfig).toEqual({
expect(result.spotterSidebarConfig).toEqual({
enablePastConversationsSidebar: true,
spotterSidebarTitle: 'Chats',
});
});

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

it('calls handleError and strips spotterDocumentationUrl when invalid', () => {
const handleError = jest.fn();
const result = buildSpotterSidebarAppInitData(base, {
const result = buildSpotterSidebarEmbedParamsContribution({
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.embedParams?.spotterSidebarConfig?.spotterDocumentationUrl).toBeUndefined();
expect(result.spotterSidebarConfig?.spotterDocumentationUrl).toBeUndefined();
});

it('returns base with visualOverridesParams when only visualOverrides is provided', () => {
it('returns visualOverridesParams when only visualOverrides is provided', () => {
const visualOverrides = {
chart: {
legend: { show: true, position: 'bottom' as const },
},
};
const result = buildSpotterSidebarAppInitData(base, {
const result = buildSpotterSidebarEmbedParamsContribution({
visualOverrides,
}, noopError);
expect(result).toEqual({
...base,
embedParams: { visualOverridesParams: visualOverrides },
});
expect(result).toEqual({ visualOverridesParams: visualOverrides });
});

it('includes visualOverridesParams with spotterSidebarConfig', () => {
Expand All @@ -75,12 +71,12 @@ describe('buildSpotterSidebarAppInitData', () => {
display: { tableTheme: 'ZEBRA' },
},
};
const result = buildSpotterSidebarAppInitData(base, {
const result = buildSpotterSidebarEmbedParamsContribution({
spotterSidebarConfig: { enablePastConversationsSidebar: true },
visualOverrides,
}, noopError);
expect(result.embedParams?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result.embedParams?.visualOverridesParams).toEqual(visualOverrides);
expect(result.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result.visualOverridesParams).toEqual(visualOverrides);
});

it('includes visualOverridesParams with standalone enablePastConversationsSidebar flag', () => {
Expand All @@ -89,20 +85,20 @@ describe('buildSpotterSidebarAppInitData', () => {
legend: { show: false },
},
};
const result = buildSpotterSidebarAppInitData(base, {
const result = buildSpotterSidebarEmbedParamsContribution({
enablePastConversationsSidebar: true,
visualOverrides,
}, noopError);
expect(result.embedParams?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result.embedParams?.visualOverridesParams).toEqual(visualOverrides);
expect(result.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result.visualOverridesParams).toEqual(visualOverrides);
});

it('does not include visualOverridesParams when it is undefined', () => {
const result = buildSpotterSidebarAppInitData(base, {
const result = buildSpotterSidebarEmbedParamsContribution({
spotterSidebarConfig: { enablePastConversationsSidebar: true },
visualOverrides: undefined,
}, noopError);
expect(result.embedParams?.visualOverridesParams).toBeUndefined();
expect(result.embedParams?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
expect(result.visualOverridesParams).toBeUndefined();
expect(result.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
});
});
76 changes: 35 additions & 41 deletions src/embed/spotter-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DefaultAppInitData, ErrorDetailsTypes, EmbedErrorCodes } from '../types';
import { ErrorDetailsTypes, EmbedErrorCodes } from '../types';
import { validateHttpUrl } from '../utils';
import { ERROR_MESSAGE } from '../errors';
import type { SpotterSidebarViewConfig } from './conversation';
import type { VisualizationOverrides } from '../types';
import type { EmbedParamsContribution } from './embedParams-builder';

/**
* Resolves enablePastConversationsSidebar with
Expand All @@ -18,20 +19,14 @@ export const resolveEnablePastConversationsSidebar = (params: {
: params.standaloneValue
);

export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
defaultAppInitData: T,
export function buildSpotterSidebarEmbedParamsContribution(
viewConfig: {
spotterSidebarConfig?: SpotterSidebarViewConfig;
enablePastConversationsSidebar?: boolean;
visualOverrides?: VisualizationOverrides;
},
handleError: (err: any) => void,
): T & {
embedParams?: {
spotterSidebarConfig?: SpotterSidebarViewConfig;
visualOverridesParams?: VisualizationOverrides | null;
};
} {
): Partial<EmbedParamsContribution> {
const { spotterSidebarConfig, enablePastConversationsSidebar, visualOverrides } = viewConfig;

const resolvedEnablePastConversations = resolveEnablePastConversationsSidebar({
Expand All @@ -40,42 +35,41 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
});

const hasConfig = spotterSidebarConfig || resolvedEnablePastConversations !== undefined;
if (!hasConfig) {
if (visualOverrides === undefined) {
return defaultAppInitData;
}
return {
...defaultAppInitData,
embedParams: { visualOverridesParams: visualOverrides },
};
if (!hasConfig && visualOverrides === undefined) {
return {};
}

const resolvedSidebarConfig: SpotterSidebarViewConfig = {
...spotterSidebarConfig,
...(resolvedEnablePastConversations !== undefined && {
enablePastConversationsSidebar: resolvedEnablePastConversations,
}),
};
const contribution: Partial<EmbedParamsContribution> = {};

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

if (resolvedSidebarConfig.spotterDocumentationUrl !== undefined) {
const [isValid, validationError] = validateHttpUrl(resolvedSidebarConfig.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 resolvedSidebarConfig.spotterDocumentationUrl;
if (resolvedSidebarConfig.spotterDocumentationUrl !== undefined) {
const [isValid, validationError] = validateHttpUrl(
resolvedSidebarConfig.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 resolvedSidebarConfig.spotterDocumentationUrl;
}
}

contribution.spotterSidebarConfig = resolvedSidebarConfig;
}

if (visualOverrides !== undefined) {
contribution.visualOverridesParams = visualOverrides;
}

return {
...defaultAppInitData,
embedParams: {
...((defaultAppInitData as any).embedParams || {}),
spotterSidebarConfig: resolvedSidebarConfig,
...(visualOverrides !== undefined ? { visualOverridesParams: visualOverrides } : {}),
},
};
return contribution;
}
Loading
Loading