You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2021/01/21 04:58:16 UTC

[superset] branch master updated: feat(native-filters): Show alert for unsaved filters after cancelling Filter Config Modal (#12554)

This is an automated email from the ASF dual-hosted git repository.

junlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new c72c39b  feat(native-filters): Show alert for unsaved filters after cancelling Filter Config Modal (#12554)
c72c39b is described below

commit c72c39bffd2ac881629b35b906fe6400c4bfe5f2
Author: Agata Stawarz <47...@users.noreply.github.com>
AuthorDate: Thu Jan 21 05:57:28 2021 +0100

    feat(native-filters): Show alert for unsaved filters after cancelling Filter Config Modal (#12554)
    
    * Add Alert when native filter is canceled and not saved
    
    * Improve styles and setting styles visible
    
    * Improve displaying filter names
    
    * Add tests for canceling native filter modal
    
    * Fix linter errors
    
    * Refactor Cancel Confirmation Alert
---
 .../nativeFilters/NativeFiltersModal_spec.tsx      |  49 ++++++++++
 .../nativeFilters/CancelConfirmationAlert.tsx      | 105 +++++++++++++++++++++
 .../components/nativeFilters/FilterConfigModal.tsx |  70 ++++++++++++--
 3 files changed, 215 insertions(+), 9 deletions(-)

diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
index e075393..d3f552f 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
@@ -19,7 +19,9 @@
 import React from 'react';
 import { styledMount as mount } from 'spec/helpers/theming';
 import { act } from 'react-dom/test-utils';
+import { ReactWrapper } from 'enzyme';
 import { Provider } from 'react-redux';
+import Alert from 'react-bootstrap/lib/Alert';
 import { FilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterConfigModal';
 import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
 import { mockStore } from 'spec/fixtures/mockStore';
@@ -74,4 +76,51 @@ describe('FiltersConfigModal', () => {
     await waitForComponentToPaint(wrapper);
     expect(onSave.mock.calls).toHaveLength(0);
   });
+
+  describe('when click cancel', () => {
+    let onCancel: jest.Mock;
+    let wrapper: ReactWrapper;
+
+    beforeEach(() => {
+      onCancel = jest.fn();
+      wrapper = setup({ onCancel, createNewOnOpen: false });
+    });
+
+    async function clickCancel() {
+      act(() => {
+        wrapper.find('.ant-modal-footer button').at(0).simulate('click');
+      });
+      await waitForComponentToPaint(wrapper);
+    }
+
+    function addFilter() {
+      act(() => {
+        wrapper.find('button[aria-label="Add tab"]').at(0).simulate('click');
+      });
+    }
+
+    it('does not show alert when there is no unsaved filters', async () => {
+      await clickCancel();
+      expect(onCancel.mock.calls).toHaveLength(1);
+    });
+
+    it('shows correct alert message for an unsaved filter', async () => {
+      addFilter();
+      await clickCancel();
+      expect(onCancel.mock.calls).toHaveLength(0);
+      expect(wrapper.find(Alert).text()).toContain(
+        'Are you sure you want to cancel? "New Filter" will not be saved.',
+      );
+    });
+
+    it('shows correct alert message for 2 unsaved filters', async () => {
+      addFilter();
+      addFilter();
+      await clickCancel();
+      expect(onCancel.mock.calls).toHaveLength(0);
+      expect(wrapper.find(Alert).text()).toContain(
+        'Are you sure you want to cancel? "New Filter" and "New Filter" will not be saved.',
+      );
+    });
+  });
 });
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/CancelConfirmationAlert.tsx b/superset-frontend/src/dashboard/components/nativeFilters/CancelConfirmationAlert.tsx
new file mode 100644
index 0000000..96d1307
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/CancelConfirmationAlert.tsx
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { styled, t } from '@superset-ui/core';
+import Alert from 'react-bootstrap/lib/Alert';
+import Button from 'src/components/Button';
+import Icon from 'src/components/Icon';
+
+const StyledAlert = styled(Alert)`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
+  padding: ${({ theme }) => theme.gridUnit * 2}px;
+`;
+
+const StyledTextContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  margin-right: ${({ theme }) => theme.gridUnit}px;
+`;
+
+const StyledTitleBox = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const StyledAlertTitle = styled.span`
+  font-weight: ${({ theme }) => theme.typography.weights.bold};
+`;
+
+const StyledAlertText = styled.p`
+  margin-left: ${({ theme }) => theme.gridUnit * 9}px;
+`;
+
+const StyledButtonsContainer = styled.div`
+  display: flex;
+  flex-direction: row;
+`;
+
+const StyledAlertIcon = styled(Icon)`
+  color: ${({ theme }) => theme.colors.alert.base};
+  margin-right: ${({ theme }) => theme.gridUnit * 3}px;
+`;
+
+export interface ConfirmationAlertProps {
+  title: string;
+  children: React.ReactNode;
+  onConfirm: () => void;
+  onDismiss: () => void;
+}
+
+export function CancelConfirmationAlert({
+  title,
+  onConfirm,
+  onDismiss,
+  children,
+}: ConfirmationAlertProps) {
+  return (
+    <StyledAlert bsStyle="warning" key="alert">
+      <StyledTextContainer>
+        <StyledTitleBox>
+          <StyledAlertIcon name="alert-solid" />
+          <StyledAlertTitle>{title}</StyledAlertTitle>
+        </StyledTitleBox>
+        <StyledAlertText>{children}</StyledAlertText>
+      </StyledTextContainer>
+      <StyledButtonsContainer>
+        <Button
+          key="submit"
+          buttonSize="small"
+          buttonStyle="primary"
+          onClick={onConfirm}
+        >
+          {t('Yes, cancel')}
+        </Button>
+        <Button
+          key="cancel"
+          buttonSize="small"
+          buttonStyle="secondary"
+          onClick={onDismiss}
+        >
+          {t('Keep editing')}
+        </Button>
+      </StyledButtonsContainer>
+    </StyledAlert>
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx
index f93e542..a7d178e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx
@@ -30,6 +30,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
 import { useFilterConfigMap, useFilterConfiguration } from './state';
 import FilterConfigForm from './FilterConfigForm';
 import { FilterConfiguration, NativeFiltersForm } from './types';
+import { CancelConfirmationAlert } from './CancelConfirmationAlert';
 
 // how long to show the "undo" button when removing a filter
 const REMOVAL_DELAY_SECS = 5;
@@ -174,6 +175,8 @@ export function FilterConfigModal({
     Record<string, FilterRemoval>
   >({});
 
+  const [saveAlertVisible, setSaveAlertVisible] = useState<boolean>(false);
+
   // brings back a filter that was previously removed ("Undo")
   const restoreFilter = useCallback(
     (id: string) => {
@@ -231,6 +234,7 @@ export function FilterConfigModal({
     const newFilterId = generateFilterId();
     setNewFilterIds([...newFilterIds, newFilterId]);
     setCurrentFilterId(newFilterId);
+    setSaveAlertVisible(false);
   }, [newFilterIds, setCurrentFilterId]);
 
   // if this is a "create" modal rather than an "edit" modal,
@@ -248,6 +252,7 @@ export function FilterConfigModal({
     setNewFilterIds([]);
     setCurrentFilterId(getInitialCurrentFilterId());
     setRemovedFilters({});
+    setSaveAlertVisible(false);
   }, [form, getInitialCurrentFilterId]);
 
   const completeFilterRemoval = (filterId: string) => {
@@ -272,6 +277,7 @@ export function FilterConfigModal({
         ...removedFilters,
         [filterId]: { isPending: true, timerId },
       }));
+      setSaveAlertVisible(false);
     } else if (action === 'add') {
       addFilter();
     }
@@ -414,11 +420,63 @@ export function FilterConfigModal({
     validateForm,
   ]);
 
-  const handleCancel = () => {
+  const confirmCancel = () => {
     resetForm();
     onCancel();
   };
 
+  const unsavedFiltersIds = newFilterIds.filter(id => !removedFilters[id]);
+
+  const getUnsavedFilterNames = (): string => {
+    const unsavedFiltersNames = unsavedFiltersIds.map(
+      id => `"${getFilterTitle(id)}"`,
+    );
+
+    if (unsavedFiltersNames.length === 0) {
+      return '';
+    }
+
+    if (unsavedFiltersNames.length === 1) {
+      return unsavedFiltersNames[0];
+    }
+
+    const lastFilter = unsavedFiltersNames.pop();
+
+    return `${unsavedFiltersNames.join(', ')} ${t('and')} ${lastFilter}`;
+  };
+
+  const handleCancel = () => {
+    if (unsavedFiltersIds.length > 0) {
+      setSaveAlertVisible(true);
+    } else {
+      confirmCancel();
+    }
+  };
+
+  const renderFooterElements = (): React.ReactNode[] => {
+    if (saveAlertVisible) {
+      return [
+        <CancelConfirmationAlert
+          title={`${unsavedFiltersIds.length} ${t('unsaved filters')}`}
+          onConfirm={confirmCancel}
+          onDismiss={() => setSaveAlertVisible(false)}
+        >
+          {t(`Are you sure you want to cancel?`)} {getUnsavedFilterNames()}{' '}
+          {t(`will not be saved.`)}
+        </CancelConfirmationAlert>,
+      ];
+    }
+
+    return [
+      <Button key="cancel" buttonStyle="secondary" onClick={handleCancel}>
+        {t('Cancel')}
+      </Button>,
+      <Button key="submit" buttonStyle="primary" onClick={onOk}>
+        {t('Save')}
+      </Button>,
+    ];
+  };
+
   return (
     <StyledModal
       visible={isOpen}
@@ -428,14 +486,7 @@ export function FilterConfigModal({
       onOk={onOk}
       centered
       data-test="filter-modal"
-      footer={[
-        <Button key="cancel" buttonStyle="secondary" onClick={handleCancel}>
-          {t('Cancel')}
-        </Button>,
-        <Button key="submit" buttonStyle="primary" onClick={onOk}>
-          {t('Save')}
-        </Button>,
-      ]}
+      footer={renderFooterElements()}
     >
       <ErrorBoundary>
         <StyledModalBody>
@@ -451,6 +502,7 @@ export function FilterConfigModal({
                 // we only need to set this if a name changed
                 setFormValues(values);
               }
+              setSaveAlertVisible(false);
             }}
             layout="vertical"
           >