Browse Source

fix(widget-builder): Add alert dialog for unsaved changes (#35274)

Nar Saynorath 2 years ago
parent
commit
394d8c8ccf

+ 19 - 0
static/app/views/dashboardsV2/widgetBuilder/widgetBuilder.tsx

@@ -185,6 +185,7 @@ function WidgetBuilder({
   end,
   statsPeriod,
   onSave,
+  route,
   router,
   tags,
 }: Props) {
@@ -231,6 +232,7 @@ function WidgetBuilder({
 
   const api = useApi();
 
+  const [isSubmitting, setIsSubmitting] = useState(false);
   const [state, setState] = useState<State>(() => {
     const defaultState: State = {
       title: defaultTitle ?? t('Custom Widget'),
@@ -401,6 +403,17 @@ function WidgetBuilder({
     fetchOrgMembers(api, organization.slug, selection.projects?.map(String));
   }, [selection.projects, api, organization.slug]);
 
+  useEffect(() => {
+    const onUnload = () => {
+      if (!isSubmitting && state.userHasModified) {
+        return t('You have unsaved changes, are you sure you want to leave?');
+      }
+      return undefined;
+    };
+
+    router.setRouteLeaveHook(route, onUnload);
+  }, [isSubmitting, state.userHasModified, route, router]);
+
   const widgetType =
     state.dataSet === DataSet.EVENTS
       ? WidgetType.DISCOVER
@@ -540,6 +553,7 @@ function WidgetBuilder({
         }
       }
 
+      set(newState, 'userHasModified', true);
       return {...newState, errors: undefined};
     });
   }
@@ -577,6 +591,9 @@ function WidgetBuilder({
     setState(prevState => {
       const newState = cloneDeep(prevState);
       set(newState, field, value);
+      if (field === 'title') {
+        set(newState, 'userHasModified', true);
+      }
       return {...newState, errors: undefined};
     });
 
@@ -856,6 +873,7 @@ function WidgetBuilder({
       return;
     }
 
+    setIsSubmitting(true);
     let nextWidgetList = [...dashboard.widgets];
     const updateWidgetIndex = getUpdateWidgetIndex();
     nextWidgetList.splice(updateWidgetIndex, 1);
@@ -900,6 +918,7 @@ function WidgetBuilder({
       });
     }
 
+    setIsSubmitting(true);
     if (notDashboardsOrigin) {
       submitFromSelectedDashboard(widgetData);
       return;

+ 47 - 0
tests/js/spec/views/dashboardsV2/widgetBuilder/widgetBuilder.spec.tsx

@@ -1288,6 +1288,53 @@ describe('WidgetBuilder', function () {
       await screen.findByText('Limit to 2 results');
     });
 
+    it('alerts the user if there are unsaved changes', async function () {
+      const {router} = renderTestComponent();
+
+      const alertMock = jest.fn();
+      const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
+      setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
+        alertMock();
+      });
+
+      const customWidgetLabels = await screen.findAllByText('Custom Widget');
+      // EditableText and chart title
+      expect(customWidgetLabels).toHaveLength(2);
+
+      // Change title text
+      userEvent.click(customWidgetLabels[0]);
+      userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
+      userEvent.paste(
+        screen.getByRole('textbox', {name: 'Widget title'}),
+        'Unique Users'
+      );
+      userEvent.keyboard('{enter}');
+
+      // Click Cancel
+      userEvent.click(screen.getByText('Cancel'));
+
+      // Assert an alert was triggered
+      expect(alertMock).toHaveBeenCalled();
+    });
+
+    it('does not trigger alert dialog if no changes', async function () {
+      const {router} = renderTestComponent();
+
+      const alertMock = jest.fn();
+      const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
+      setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
+        alertMock();
+      });
+
+      await screen.findAllByText('Custom Widget');
+
+      // Click Cancel
+      userEvent.click(screen.getByText('Cancel'));
+
+      // Assert an alert was triggered
+      expect(alertMock).not.toHaveBeenCalled();
+    });
+
     describe('Sort by selectors', function () {
       it('renders', async function () {
         renderTestComponent({