This is an automated email from the ASF dual-hosted git repository. villebro pushed a commit to branch 1.0 in repository https://gitbox.apache.org/repos/asf/superset.git
commit 5e4ce48ccfcee267ee785981a983c0fbd7187650 Author: Agata Stawarz <[email protected]> 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 40a644a..2be67db 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx @@ -31,6 +31,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; @@ -175,6 +176,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) => { @@ -232,6 +235,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, @@ -249,6 +253,7 @@ export function FilterConfigModal({ setNewFilterIds([]); setCurrentFilterId(getInitialCurrentFilterId()); setRemovedFilters({}); + setSaveAlertVisible(false); }, [form, getInitialCurrentFilterId]); const completeFilterRemoval = (filterId: string) => { @@ -273,6 +278,7 @@ export function FilterConfigModal({ ...removedFilters, [filterId]: { isPending: true, timerId }, })); + setSaveAlertVisible(false); } else if (action === 'add') { addFilter(); } @@ -418,11 +424,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} @@ -432,14 +490,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> @@ -455,6 +506,7 @@ export function FilterConfigModal({ // we only need to set this if a name changed setFormValues(values); } + setSaveAlertVisible(false); }} layout="vertical" >
