fix: Dropdown menu not closing on Dashboard view (#11671)

This commit is contained in:
Kamil Gabryjelski 2020-11-12 23:48:48 +01:00 committed by GitHub
parent 5cb6c25ca0
commit 3ad65bc163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 250 additions and 241 deletions

View File

@ -70,27 +70,24 @@ describe('Dashboard top-level controls', () => {
.find('.world_map')
.should('be.exist');
cy.get(`#slice_${mapId}-controls`).click();
cy.get(`#slice_${mapId}-controls`)
.next()
.find('[data-test="dashboard-slice-refresh-tooltip"]')
.trigger('click', { force: true });
cy.get(`[data-test="slice_${mapId}-menu"]`)
.find('[data-test="refresh-dashboard-menu-item"]')
.click({ force: true });
// not allow dashboard level force refresh when any chart is loading
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'have.class',
'ant-menu-item-disabled',
'ant-dropdown-menu-item-disabled',
);
// not allow chart level force refresh when it is loading
cy.get(`#slice_${mapId}-controls`)
.next()
.find('[data-test="dashboard-slice-refresh-tooltip"]')
.parent()
.should('have.class', 'ant-menu-item-disabled');
cy.get(`[data-test="slice_${mapId}-menu"]`)
.find('[data-test="refresh-dashboard-menu-item"]')
.should('have.class', 'ant-dropdown-menu-item-disabled');
cy.wait(`@postJson_${mapId}_force`);
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
'ant-menu-item-disabled',
'ant-dropdown-menu-item-disabled',
);
});
@ -100,15 +97,15 @@ describe('Dashboard top-level controls', () => {
cy.get('[data-test="more-horiz"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
'ant-menu-item-disabled',
'ant-dropdown-menu-item-disabled',
);
// wait the all dash finish loading.
cy.wait(sliceRequests);
cy.get('[data-test="refresh-dashboard-menu-item"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').click({ force: true });
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'have.class',
'ant-menu-item-disabled',
'ant-dropdown-menu-item-disabled',
);
// wait all charts force refreshed
@ -124,7 +121,7 @@ describe('Dashboard top-level controls', () => {
cy.get('[data-test="more-horiz"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
'ant-menu-item-disabled',
'ant-dropdown-menu-item-disabled',
);
});
});

View File

@ -64,7 +64,9 @@ function openAdvancedProperties() {
function openDashboardEditProperties() {
// open dashboard properties edit modal
cy.get('#save-dash-split-button').trigger('click', { force: true });
cy.get('.dropdown-menu').contains('Edit dashboard properties').click();
cy.get('[data-test=header-actions-menu]')
.contains('Edit dashboard properties')
.click({ force: true });
}
describe('Dashboard edit action', () => {

View File

@ -33,6 +33,11 @@ describe('Dashboard edit markdown', () => {
cy.get('[data-test="dashboard-header"]')
.find('[data-test="edit-alt"]')
.click();
// lazy load - need to open dropdown for the scripts to load
cy.get('[data-test="dashboard-header"]')
.find('[data-test="more-horiz"]')
.click();
cy.get('script').then(nodes => {
// load 5 new script chunks for css editor
expect(nodes.length).to.greaterThan(numScripts);

View File

@ -18,8 +18,7 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
import { DropdownButton } from 'react-bootstrap';
import { Menu } from 'src/common/components';
import { Menu, NoAnimationDropdown } from 'src/common/components';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
import URLShortLinkModal from 'src/components/URLShortLinkModal';
import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown';
@ -57,40 +56,43 @@ describe('HeaderActionsDropdown', () => {
const wrapper = shallow(
<HeaderActionsDropdown {...props} {...overrideProps} />,
);
return wrapper;
const menu = shallow(
<div>{wrapper.find(NoAnimationDropdown).props().overlay}</div>,
);
return { wrapper, menu };
}
describe('readonly-user', () => {
const overrideProps = { userCanSave: false };
it('should render the DropdownButton', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(DropdownButton)).toExist();
const { wrapper } = setup(overrideProps);
expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should not render the SaveModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(SaveModal)).not.toExist();
const { menu } = setup(overrideProps);
expect(menu.find(SaveModal)).not.toExist();
});
it('should render five Menu items', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(Menu.Item)).toHaveLength(5);
const { menu } = setup(overrideProps);
expect(menu.find(Menu.Item)).toHaveLength(5);
});
it('should render the RefreshIntervalModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(RefreshIntervalModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(URLShortLinkModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(URLShortLinkModal)).toExist();
});
it('should not render the CssEditor', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(CssEditor)).not.toExist();
const { menu } = setup(overrideProps);
expect(menu.find(CssEditor)).not.toExist();
});
});
@ -98,33 +100,33 @@ describe('HeaderActionsDropdown', () => {
const overrideProps = { userCanSave: true };
it('should render the DropdownButton', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(DropdownButton)).toExist();
const { wrapper } = setup(overrideProps);
expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should render the SaveModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(SaveModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(SaveModal)).toExist();
});
it('should render six Menu items', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(Menu.Item)).toHaveLength(6);
const { menu } = setup(overrideProps);
expect(menu.find(Menu.Item)).toHaveLength(6);
});
it('should render the RefreshIntervalModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(RefreshIntervalModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(URLShortLinkModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(URLShortLinkModal)).toExist();
});
it('should not render the CssEditor', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(CssEditor)).not.toExist();
const { menu } = setup(overrideProps);
expect(menu.find(CssEditor)).not.toExist();
});
});
@ -132,33 +134,33 @@ describe('HeaderActionsDropdown', () => {
const overrideProps = { userCanSave: true, editMode: true };
it('should render the DropdownButton', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(DropdownButton)).toExist();
const { wrapper } = setup(overrideProps);
expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should render the SaveModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(SaveModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(SaveModal)).toExist();
});
it('should render seven MenuItems', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(Menu.Item)).toHaveLength(7);
const { menu } = setup(overrideProps);
expect(menu.find(Menu.Item)).toHaveLength(7);
});
it('should render the RefreshIntervalModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(RefreshIntervalModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(URLShortLinkModal)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(URLShortLinkModal)).toExist();
});
it('should render the CssEditor', () => {
const wrapper = setup(overrideProps);
expect(wrapper.find(CssEditor)).toExist();
const { menu } = setup(overrideProps);
expect(menu.find(CssEditor)).toExist();
});
});
});

View File

@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { styled } from '@superset-ui/core';
// eslint-disable-next-line no-restricted-imports
import { Skeleton, Menu as AntdMenu } from 'antd';
import { Dropdown, Skeleton, Menu as AntdMenu } from 'antd';
import { DropDownProps } from 'antd/lib/dropdown';
/*
Antd is re-exported from here so we can override components with Emotion as needed.
@ -57,6 +59,13 @@ export const Menu = Object.assign(AntdMenu, {
Item: MenuItem,
});
export const NoAnimationDropdown = (props: DropDownProps) => (
<Dropdown
overlayStyle={{ zIndex: 4000, animationDuration: '0s' }}
{...props}
/>
);
export const ThinSkeleton = styled(Skeleton)`
h3 {
margin: ${({ theme }) => theme.gridUnit}px 0;

View File

@ -102,9 +102,9 @@ export default class ModalTrigger extends React.Component {
/* eslint-disable jsx-a11y/interactive-supports-focus */
return (
<>
<span onClick={this.open} role="button">
<div onClick={this.open} role="button">
{this.props.triggerNode}
</span>
</div>
{this.renderModal()}
</>
);

View File

@ -19,10 +19,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SupersetClient, t } from '@superset-ui/core';
import { DropdownButton } from 'react-bootstrap';
import { styled, SupersetClient, t } from '@superset-ui/core';
import { Menu } from 'src/common/components';
import { Menu, NoAnimationDropdown } from 'src/common/components';
import Icon from 'src/components/Icon';
import CssEditor from './CssEditor';
@ -84,6 +83,10 @@ const MENU_KEYS = {
TOGGLE_FULLSCREEN: 'toggle-fullscreen',
};
const DropdownButton = styled.div`
margin-left: ${({ theme }) => theme.gridUnit * 2.5}px;
`;
class HeaderActionsDropdown extends React.PureComponent {
static discardChanges() {
window.location.reload();
@ -188,112 +191,114 @@ class HeaderActionsDropdown extends React.PureComponent {
const emailSubject = `${emailTitle} ${dashboardTitle}`;
const emailBody = t('Check out this dashboard: ');
return (
<DropdownButton
title={<Icon name="more-horiz" />}
noCaret
id="save-dash-split-button"
bsSize="large"
style={{ border: 'none', padding: 0, marginLeft: '4px' }}
pullRight
const menu = (
<Menu
onClick={this.handleMenuClick}
selectable={false}
data-test="header-actions-menu"
>
<Menu onClick={this.handleMenuClick} selectable={false}>
{userCanSave && (
<Menu.Item key={MENU_KEYS.SAVE_MODAL}>
<SaveModal
addSuccessToast={this.props.addSuccessToast}
addDangerToast={this.props.addDangerToast}
dashboardId={dashboardId}
dashboardTitle={dashboardTitle}
dashboardInfo={dashboardInfo}
saveType={SAVE_TYPE_NEWDASHBOARD}
layout={layout}
expandedSlices={expandedSlices}
refreshFrequency={refreshFrequency}
shouldPersistRefreshFrequency={shouldPersistRefreshFrequency}
lastModifiedTime={lastModifiedTime}
customCss={customCss}
colorNamespace={colorNamespace}
colorScheme={colorScheme}
onSave={onSave}
triggerNode={
<span data-test="save-as-menu-item">{t('Save as')}</span>
}
canOverwrite={userCanEdit}
/>
</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.SHARE_DASHBOARD}>
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
getActiveFilters(),
window.location.hash,
)}
emailSubject={emailSubject}
emailContent={emailBody}
{userCanSave && (
<Menu.Item key={MENU_KEYS.SAVE_MODAL}>
<SaveModal
addSuccessToast={this.props.addSuccessToast}
addDangerToast={this.props.addDangerToast}
triggerNode={<span>{t('Share dashboard')}</span>}
/>
</Menu.Item>
<Menu.Item
key={MENU_KEYS.REFRESH_DASHBOARD}
data-test="refresh-dashboard-menu-item"
disabled={isLoading}
>
{t('Refresh dashboard')}
</Menu.Item>
<Menu.Divider />
<Menu.Item key={MENU_KEYS.AUTOREFRESH_MODAL}>
<RefreshIntervalModal
dashboardId={dashboardId}
dashboardTitle={dashboardTitle}
dashboardInfo={dashboardInfo}
saveType={SAVE_TYPE_NEWDASHBOARD}
layout={layout}
expandedSlices={expandedSlices}
refreshFrequency={refreshFrequency}
refreshLimit={refreshLimit}
refreshWarning={refreshWarning}
onChange={this.changeRefreshInterval}
editMode={editMode}
triggerNode={<span>{t('Set auto-refresh interval')}</span>}
shouldPersistRefreshFrequency={shouldPersistRefreshFrequency}
lastModifiedTime={lastModifiedTime}
customCss={customCss}
colorNamespace={colorNamespace}
colorScheme={colorScheme}
onSave={onSave}
triggerNode={
<span data-test="save-as-menu-item">{t('Save as')}</span>
}
canOverwrite={userCanEdit}
/>
</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.SHARE_DASHBOARD}>
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
getActiveFilters(),
window.location.hash,
)}
emailSubject={emailSubject}
emailContent={emailBody}
addDangerToast={this.props.addDangerToast}
triggerNode={<span>{t('Share dashboard')}</span>}
/>
</Menu.Item>
<Menu.Item
key={MENU_KEYS.REFRESH_DASHBOARD}
data-test="refresh-dashboard-menu-item"
disabled={isLoading}
>
{t('Refresh dashboard')}
</Menu.Item>
<Menu.Divider />
<Menu.Item key={MENU_KEYS.AUTOREFRESH_MODAL}>
<RefreshIntervalModal
refreshFrequency={refreshFrequency}
refreshLimit={refreshLimit}
refreshWarning={refreshWarning}
onChange={this.changeRefreshInterval}
editMode={editMode}
triggerNode={<span>{t('Set auto-refresh interval')}</span>}
/>
</Menu.Item>
{editMode && (
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
<FilterScopeModal
className="m-r-5"
triggerNode={t('Set filter mapping')}
/>
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
<FilterScopeModal
className="m-r-5"
triggerNode={t('Set filter mapping')}
/>
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
{t('Edit dashboard properties')}
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
{t('Edit dashboard properties')}
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MENU_KEYS.EDIT_CSS}>
<CssEditor
triggerNode={<span>{t('Edit CSS')}</span>}
initialCss={this.state.css}
templates={this.state.cssTemplates}
onChange={this.changeCss}
/>
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MENU_KEYS.EDIT_CSS}>
<CssEditor
triggerNode={<span>{t('Edit CSS')}</span>}
initialCss={this.state.css}
templates={this.state.cssTemplates}
onChange={this.changeCss}
/>
</Menu.Item>
)}
{!editMode && (
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
{t('Download as image')}
</Menu.Item>
)}
{!editMode && (
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
{t('Download as image')}
</Menu.Item>
)}
{!editMode && (
<Menu.Item key={MENU_KEYS.TOGGLE_FULLSCREEN}>
{t('Toggle FullScreen')}
</Menu.Item>
)}
</Menu>
</DropdownButton>
{!editMode && (
<Menu.Item key={MENU_KEYS.TOGGLE_FULLSCREEN}>
{t('Toggle FullScreen')}
</Menu.Item>
)}
</Menu>
);
return (
<NoAnimationDropdown overlay={menu} trigger={['click']}>
<DropdownButton id="save-dash-split-button" role="button">
<Icon name="more-horiz" />
</DropdownButton>
</NoAnimationDropdown>
);
}
}

View File

@ -19,9 +19,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { DropdownButton } from 'react-bootstrap';
import { styled, t } from '@superset-ui/core';
import { Menu } from 'src/common/components';
import { Menu, NoAnimationDropdown } from 'src/common/components';
import URLShortLinkModal from '../../components/URLShortLinkModal';
import downloadAsImage from '../../utils/downloadAsImage';
import getDashboardUrl from '../util/getDashboardUrl';
@ -82,6 +81,12 @@ const VerticalDotsContainer = styled.div`
}
`;
const RefreshTooltip = styled.div`
height: ${({ theme }) => theme.gridUnit * 4}px;
margin: ${({ theme }) => theme.gridUnit}px 0;
color: ${({ theme }) => theme.colors.grayscale.base};
`;
const VerticalDotsTrigger = () => (
<VerticalDotsContainer>
<span className="dot" />
@ -161,70 +166,77 @@ class SliceHeaderControls extends React.PureComponent {
? t('Cached %s', cachedWhen)
: (updatedWhen && t('Fetched %s', updatedWhen)) || '';
const resizeLabel = isFullSize ? t('Minimize') : t('Maximize');
return (
<DropdownButton
id={`slice_${slice.slice_id}-controls`}
pullRight
noCaret
title={<VerticalDotsTrigger />}
style={{ padding: 0 }}
// react-bootstrap handles visibility, but call toggle to force a re-render
// and update the fetched/cached timestamps
onToggle={this.toggleControls}
const menu = (
<Menu
onClick={this.handleMenuClick}
selectable={false}
data-test={`slice_${slice.slice_id}-menu`}
>
<Menu onClick={this.handleMenuClick} selectable={false}>
<Menu.Item
key={MENU_KEYS.FORCE_REFRESH}
disabled={this.props.chartStatus === 'loading'}
style={{ height: 'auto', lineHeight: 'initial' }}
>
{t('Force refresh')}
<div
className="refresh-tooltip"
data-test="dashboard-slice-refresh-tooltip"
>
{refreshTooltip}
</div>
<Menu.Item
key={MENU_KEYS.FORCE_REFRESH}
disabled={this.props.chartStatus === 'loading'}
style={{ height: 'auto', lineHeight: 'initial' }}
data-test="refresh-dashboard-menu-item"
>
{t('Force refresh')}
<RefreshTooltip data-test="dashboard-slice-refresh-tooltip">
{refreshTooltip}
</RefreshTooltip>
</Menu.Item>
<Menu.Divider />
{slice.description && (
<Menu.Item key={MENU_KEYS.TOGGLE_CHART_DESCRIPTION}>
{t('Toggle chart description')}
</Menu.Item>
)}
<Menu.Divider />
{slice.description && (
<Menu.Item key={MENU_KEYS.TOGGLE_CHART_DESCRIPTION}>
{t('Toggle chart description')}
</Menu.Item>
)}
{this.props.supersetCanExplore && (
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}>
{t('Explore chart')}
</Menu.Item>
)}
{this.props.supersetCanCSV && (
<Menu.Item key={MENU_KEYS.EXPORT_CSV}>{t('Export CSV')}</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.RESIZE_LABEL}>{resizeLabel}</Menu.Item>
<Menu.Item key={MENU_KEYS.SHARE_CHART}>
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
getActiveFilters(),
componentId,
)}
addDangerToast={addDangerToast}
title={t('Share chart')}
triggerNode={<span>{t('Share chart')}</span>}
/>
{this.props.supersetCanExplore && (
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}>
{t('Explore chart')}
</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
{t('Download as image')}
</Menu.Item>
</Menu>
</DropdownButton>
{this.props.supersetCanCSV && (
<Menu.Item key={MENU_KEYS.EXPORT_CSV}>{t('Export CSV')}</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.RESIZE_LABEL}>{resizeLabel}</Menu.Item>
<Menu.Item key={MENU_KEYS.SHARE_CHART}>
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
getActiveFilters(),
componentId,
)}
addDangerToast={addDangerToast}
title={t('Share chart')}
triggerNode={<span>{t('Share chart')}</span>}
/>
</Menu.Item>
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
{t('Download as image')}
</Menu.Item>
</Menu>
);
return (
<NoAnimationDropdown
overlay={menu}
trigger={['click']}
placement="bottomRight"
dropdownAlign={{
offset: [-40, 4],
}}
>
<a id={`slice_${slice.slice_id}-controls`} role="button">
<VerticalDotsTrigger />
</a>
</NoAnimationDropdown>
);
}
}

View File

@ -109,29 +109,6 @@ body {
}
}
.dashboard .dashboard-header {
#save-dash-split-button {
border-radius: 0;
margin-left: -9px;
height: 30px;
width: 30px;
&.btn.btn-primary {
border-left-color: @lightest;
}
& + .dropdown-menu.dropdown-menu-right {
min-width: unset;
}
.caret {
display: inline-block;
width: 100%;
height: 100%;
}
}
}
.dashboard .chart-header,
.dashboard .dashboard-header {
.dropdown-menu {