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"
>