diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index b6ab4de1994fdd..52b6d858f6352f 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -95,8 +95,10 @@ type Props = { handleChangeSplitDataset?: (widget: Widget, index: number) => void; isPreview?: boolean; newWidget?: Widget; + newlyAddedWidget?: Widget; onAddWidget?: (dataset: DataSet, openWidgetTemplates?: boolean) => void; onEditWidget?: (widget: Widget) => void; + onNewWidgetScrollComplete?: () => void; onSetNewWidget?: () => void; paramDashboardId?: string; paramTemplateId?: string; @@ -352,8 +354,15 @@ class Dashboard extends Component { renderWidget(widget: Widget, index: number) { const {isMobile, windowWidth} = this.state; - const {isEditingDashboard, widgetLimitReached, isPreview, dashboard, location} = - this.props; + const { + isEditingDashboard, + widgetLimitReached, + isPreview, + dashboard, + location, + newlyAddedWidget, + onNewWidgetScrollComplete, + } = this.props; const widgetProps = { widget, @@ -381,6 +390,8 @@ class Dashboard extends Component { isMobile={isMobile} windowWidth={windowWidth} index={String(index)} + newlyAddedWidget={newlyAddedWidget} + onNewWidgetScrollComplete={onNewWidgetScrollComplete} /> ); diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx index 338825427ef137..d77c4ee193bae9 100644 --- a/static/app/views/dashboards/detail.spec.tsx +++ b/static/app/views/dashboards/detail.spec.tsx @@ -284,6 +284,7 @@ describe('Dashboards > Detail', function () { let widgets!: Array>; let mockVisit!: jest.Mock; let mockPut!: jest.Mock; + let mockScrollIntoView!: jest.Mock; beforeEach(function () { window.confirm = jest.fn(); @@ -447,6 +448,9 @@ describe('Dashboards > Detail', function () { url: '/organizations/org-slug/measurements-meta/', body: [], }); + + mockScrollIntoView = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; }); afterEach(function () { @@ -2542,6 +2546,7 @@ describe('Dashboards > Detail', function () { // The widget is added in the dashboard expect(await screen.findByText('Totally new widget')).toBeInTheDocument(); + expect(mockScrollIntoView).toHaveBeenCalled(); }); it('allows for editing a widget in view mode', async function () { @@ -2599,6 +2604,7 @@ describe('Dashboards > Detail', function () { widgets: [expect.objectContaining({title: 'Updated Widget Title'})], }) ); + expect(mockScrollIntoView).toHaveBeenCalled(); }); it('allows for creating a widget in view mode', async function () { diff --git a/static/app/views/dashboards/detail.tsx b/static/app/views/dashboards/detail.tsx index a1b1a00570f1bd..18de6f6051acc9 100644 --- a/static/app/views/dashboards/detail.tsx +++ b/static/app/views/dashboards/detail.tsx @@ -137,6 +137,7 @@ type State = { modifiedDashboard: DashboardDetails | null; widgetLegendState: WidgetLegendSelectionState; widgetLimitReached: boolean; + newlyAddedWidget?: Widget; openWidgetTemplates?: boolean; } & WidgetViewerContextProps; @@ -259,6 +260,7 @@ class DashboardDetail extends Component { isSavingDashboardFilters: false, isWidgetBuilderOpen: this.isRedesignedWidgetBuilder, openWidgetTemplates: undefined, + newlyAddedWidget: undefined, }; componentDidMount() { @@ -645,6 +647,12 @@ class DashboardDetail extends Component { this.onUpdateWidget([...newModifiedDashboard.widgets, widget]); }; + handleScrollToNewWidgetComplete = () => { + this.setState({ + newlyAddedWidget: undefined, + }); + }; + onAddWidget = (dataset: DataSet, openWidgetTemplates?: boolean) => { const { organization, @@ -753,6 +761,9 @@ class DashboardDetail extends Component { addLoadingMessage(t('Saving widget')); this.handleUpdateWidgetList(newWidgets); } + this.setState({ + newlyAddedWidget: mergedWidget, + }); this.handleCloseWidgetBuilder(); } catch (error) { @@ -1079,8 +1090,14 @@ class DashboardDetail extends Component { onDashboardUpdate, projects, } = this.props; - const {modifiedDashboard, dashboardState, widgetLimitReached, seriesData, setData} = - this.state; + const { + modifiedDashboard, + dashboardState, + widgetLimitReached, + seriesData, + setData, + newlyAddedWidget, + } = this.state; const {dashboardId} = params; const hasUnsavedFilters = @@ -1280,6 +1297,10 @@ class DashboardDetail extends Component { isPreview={this.isPreview} widgetLegendState={this.state.widgetLegendState} onEditWidget={this.onEditWidget} + newlyAddedWidget={newlyAddedWidget} + onNewWidgetScrollComplete={ + this.handleScrollToNewWidgetComplete + } /> void; windowWidth?: number; }; function SortableWidget(props: Props) { + const widgetRef = useRef(null); const { widget, isEditingDashboard, @@ -52,6 +55,8 @@ function SortableWidget(props: Props) { widgetLegendState, dashboardPermissions, dashboardCreator, + newlyAddedWidget, + onNewWidgetScrollComplete, } = props; const organization = useOrganization(); @@ -65,6 +70,16 @@ function SortableWidget(props: Props) { dashboardCreator ); + useEffect(() => { + const isMatchingWidget = isEditingDashboard + ? widget.tempId === newlyAddedWidget?.tempId + : widget.id === newlyAddedWidget?.id; + if (widgetRef.current && newlyAddedWidget && isMatchingWidget) { + widgetRef.current.scrollIntoView({behavior: 'smooth', block: 'center'}); + onNewWidgetScrollComplete?.(); + } + }, [newlyAddedWidget, widget, isEditingDashboard, onNewWidgetScrollComplete]); + const widgetProps: ComponentProps = { widget, isEditingDashboard, @@ -92,7 +107,7 @@ function SortableWidget(props: Props) { }; return ( - +