Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,174 @@ describe('add to dashboard modal', () => {
});
});

describe('text widgets', () => {
let textWidget: Widget;

beforeEach(() => {
textWidget = {
title: 'My Note',
description: 'this is a text widget description',
displayType: DisplayType.TEXT,
interval: '5m',
widgetType: undefined,
queries: [],
};
});

it('renders without making an events-stats request', async () => {
render(
<AddToDashboardModal
Header={stubEl}
Footer={stubEl as ModalRenderProps['Footer']}
Body={stubEl as ModalRenderProps['Body']}
CloseButton={stubEl}
closeModal={() => undefined}
organization={initialData.organization}
widgets={[textWidget]}
selection={defaultSelection}
location={LocationFixture()}
/>
);

await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeEnabled();
});

expect(eventsStatsMock).not.toHaveBeenCalled();
});

it('adds a text widget to an existing dashboard without modifying queries', async () => {
const dashboardDetailGetMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: {id: '1', widgets: []},
});
const dashboardDetailPutMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
method: 'PUT',
body: {},
});

render(
<AddToDashboardModal
Header={stubEl}
Footer={stubEl as ModalRenderProps['Footer']}
Body={stubEl as ModalRenderProps['Body']}
CloseButton={stubEl}
closeModal={() => undefined}
organization={initialData.organization}
widgets={[textWidget]}
selection={defaultSelection}
location={LocationFixture()}
/>
);

await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeEnabled();
});
await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
await userEvent.click(screen.getByText('Add + Stay on this Page'));

expect(dashboardDetailGetMock).toHaveBeenCalled();
await waitFor(() => {
expect(dashboardDetailPutMock).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/1/',
expect.objectContaining({
data: expect.objectContaining({
widgets: [
expect.objectContaining({
title: 'My Note',
description: 'this is a text widget description',
displayType: DisplayType.TEXT,
queries: [],
layout: expect.any(Object),
}),
],
}),
})
);
});
});

it('navigates to new dashboard with text widget in location state', async () => {
const {router} = render(
<AddToDashboardModal
Header={stubEl}
Footer={stubEl as ModalRenderProps['Footer']}
Body={stubEl as ModalRenderProps['Body']}
CloseButton={stubEl}
closeModal={() => undefined}
organization={initialData.organization}
widgets={[textWidget]}
selection={defaultSelection}
actions={['add-and-open-dashboard']}
location={LocationFixture()}
/>
);

await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeEnabled();
});
await selectEvent.select(
screen.getByText('Select Dashboard'),
'+ Create New Dashboard'
);
await userEvent.click(screen.getByText('Add + Open Dashboard'));

expect(router.location.pathname).toBe('/organizations/org-slug/dashboards/new/');
expect(router.location.state?.widgets).toHaveLength(1);
expect(router.location.state?.widgets[0]).toMatchObject({
title: 'My Note',
displayType: DisplayType.TEXT,
queries: [],
});
expect(router.location.state?.widgets[0]?.layout).toMatchObject({
x: expect.any(Number),
y: expect.any(Number),
w: expect.any(Number),
h: expect.any(Number),
minH: expect.any(Number),
});
});

it('navigates to the widget builder with text widget in session storage', async () => {
const {router} = render(
<AddToDashboardModal
Header={stubEl}
Footer={stubEl as ModalRenderProps['Footer']}
Body={stubEl as ModalRenderProps['Body']}
CloseButton={stubEl}
closeModal={() => undefined}
organization={initialData.organization}
widgets={[textWidget]}
selection={defaultSelection}
actions={['open-in-widget-builder']}
location={LocationFixture()}
/>
);

await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeEnabled();
});
await selectEvent.select(
screen.getByText('Select Dashboard'),
'+ Create New Dashboard'
);
await userEvent.click(screen.getByText('Open in Widget Builder'));

expect(router.location.pathname).toBe(
'/organizations/org-slug/dashboards/new/widget-builder/widget/new/'
);

expect(router.location.query.title).toBe('My Note');
expect(router.location.query.textContent).toBeUndefined();
expect(router.location.query.description).toBeUndefined();
expect(router.location.query.displayType).toBe(DisplayType.TEXT);
expect(sessionStorage.getItem('dashboard:widget-builder:text-content')).toBe(
JSON.stringify('this is a text widget description')
);
});
});

describe('multiple widgets', () => {
let multipleWidgets: [Widget, Widget];

Expand Down
28 changes: 26 additions & 2 deletions static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type {
} from 'sentry/views/dashboards/types';
import {
DEFAULT_WIDGET_NAME,
DisplayType,
MAX_WIDGETS,
WidgetType,
} from 'sentry/views/dashboards/types';
Expand All @@ -61,7 +62,10 @@ import {
usesTimeSeriesData,
} from 'sentry/views/dashboards/utils';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import {NEW_DASHBOARD_ID} from 'sentry/views/dashboards/widgetBuilder/utils';
import {
addWidgetBuilderSessionStorageParams,
NEW_DASHBOARD_ID,
} from 'sentry/views/dashboards/widgetBuilder/utils';
import {convertWidgetToQueryParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
import WidgetCard from 'sentry/views/dashboards/widgetCard';
import {DashboardsMEPProvider} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
Expand Down Expand Up @@ -214,6 +218,10 @@ function AddToDashboardModal({

const widgetAsQueryParams = convertWidgetToQueryParams(widget);

if (page === 'builder') {
addWidgetBuilderSessionStorageParams(widget);
}

navigate(
normalizeUrl({
pathname,
Expand Down Expand Up @@ -242,6 +250,13 @@ function AddToDashboardModal({

function normalizeWidgets(widgetsToNormalize: Widget[]): Widget[] {
return widgetsToNormalize.map(w => {
if (w.displayType === DisplayType.TEXT) {
return {
...w,
title: hasMultipleWidgets ? (w.title ?? DEFAULT_WIDGET_NAME) : newWidgetTitle,
};
}

let newOrderBy = orderBy ?? w.queries[0]!.orderby;
if (!(usesTimeSeriesData(w.displayType) && w.queries[0]!.columns.length)) {
newOrderBy = ''; // Clear orderby if its not a top n visualization.
Expand Down Expand Up @@ -469,7 +484,16 @@ function AddToDashboardModal({
organization={organization}
eventView={eventViewFromWidget(
newWidgetTitle,
widget.queries[0]!,
widget.displayType === DisplayType.TEXT
? {
name: '',
fields: [],
aggregates: [],
columns: [],
orderby: '',
conditions: '',
}
: widget.queries[0]!,
Comment on lines 485 to +496
Copy link
Copy Markdown
Member Author

@nikkikapadia nikkikapadia Mar 18, 2026

Choose a reason for hiding this comment

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

it's easier to do this than to create a different modal or even do an early return because the WidgetCard needs information from these providers otherwise it won't render. Also, we want to keep using the WidgetCard because it contains the WidgetFrame and the chart with all the appropriate formatting.

selection
)}
location={location}
Expand Down
Loading