diff options
Diffstat (limited to 'server/sonar-web')
225 files changed, 6788 insertions, 6768 deletions
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index 637a95e3fca..5280e553a75 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -22,14 +22,13 @@ import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import { SuggestionLink } from './SuggestionsProvider'; import { CurrentUser, isLoggedIn } from '../../types'; -import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/urls'; +import { DropdownOverlay } from '../../../components/controls/Dropdown'; interface Props { currentUser: CurrentUser; onClose: () => void; - popupPosition?: BubblePopupPosition; suggestions: Array<SuggestionLink>; } @@ -46,7 +45,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { }; renderTitle(text: string) { - return <li className="dropdown-header">{text}</li>; + return <li className="menu-header">{text}</li>; } renderSuggestions() { @@ -147,10 +146,8 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { render() { return ( - <BubblePopup - customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup" - position={this.props.popupPosition}> - <ul className="menu"> + <DropdownOverlay> + <ul className="menu abs-width-240"> {this.renderSuggestions()} <li> <Link onClick={this.props.onClose} to="/documentation"> @@ -165,7 +162,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { {this.context.onSonarCloud && this.renderSonarCloudLinks()} {!this.context.onSonarCloud && this.renderSonarQubeLinks()} </ul> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index 29b58e4e6e3..20ddca01c15 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -21,9 +21,9 @@ import * as React from 'react'; import EmbedDocsPopup from './EmbedDocsPopup'; import { SuggestionLink } from './SuggestionsProvider'; import { CurrentUser } from '../../types'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; -import HelpIcon from '../../../components/icons-components/HelpIcon'; +import Toggler from '../../../components/controls/Toggler'; import Tooltip from '../../../components/controls/Tooltip'; +import HelpIcon from '../../../components/icons-components/HelpIcon'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -63,6 +63,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); + event.currentTarget.blur(); this.toggleHelp(); }; @@ -78,26 +79,26 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta render() { return ( - <BubblePopupHelper - isOpen={this.state.helpOpen} - offset={{ horizontal: 12, vertical: -10 }} - popup={ - <EmbedDocsPopup - currentUser={this.props.currentUser} - onClose={this.closeHelp} - suggestions={this.props.suggestions} - /> - } - position="bottomleft" - togglePopup={this.setHelpDisplay}> - <Tooltip - overlay={this.props.tooltip ? translate('tutorials.follow_later') : undefined} - visible={this.props.showTooltip}> - <a className="navbar-help" href="#" onClick={this.handleClick}> - <HelpIcon /> - </a> - </Tooltip> - </BubblePopupHelper> + <li className="dropdown"> + <Toggler + onRequestClose={this.closeHelp} + open={this.state.helpOpen} + overlay={ + <EmbedDocsPopup + currentUser={this.props.currentUser} + onClose={this.closeHelp} + suggestions={this.props.suggestions} + /> + }> + <Tooltip + overlay={this.props.tooltip ? translate('tutorials.follow_later') : undefined} + visible={this.props.showTooltip}> + <a className="navbar-help" href="#" onClick={this.handleClick}> + <HelpIcon /> + </a> + </Tooltip> + </Toggler> + </li> ); } } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap index b1af003796f..0984724bfcd 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap @@ -1,15 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display suggestion links 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup" -> +<DropdownOverlay> <ul - className="menu" + className="menu abs-width-240" > <React.Fragment> <li - className="dropdown-header" + className="menu-header" > embed_docs.suggestion </li> @@ -74,7 +72,7 @@ exports[`should display suggestion links 1`] = ` className="divider" /> <li - className="dropdown-header" + className="menu-header" > embed_docs.get_support </li> @@ -114,7 +112,7 @@ exports[`should display suggestion links 1`] = ` className="divider" /> <li - className="dropdown-header" + className="menu-header" > embed_docs.stay_connected </li> @@ -152,5 +150,5 @@ exports[`should display suggestion links 1`] = ` </li> </React.Fragment> </ul> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 890e4d5f29e..a918d734e24 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; @@ -35,6 +34,7 @@ import { import { translate } from '../../../../helpers/l10n'; import PlusCircleIcon from '../../../../components/icons-components/PlusCircleIcon'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; +import Toggler from '../../../../components/controls/Toggler'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { @@ -91,19 +91,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State } }; - renderDropdown = () => { - const { configuration } = this.props.component; - return this.state.dropdownOpen ? ( - <ComponentNavBranchesMenu - branchLikes={this.props.branchLikes} - canAdmin={configuration && configuration.showSettings} - component={this.props.component} - currentBranchLike={this.props.currentBranchLike} - onClose={this.closeDropdown} - /> - ) : null; - }; - renderMergeBranch = () => { const { currentBranchLike } = this.props; if (isShortLivingBranch(currentBranchLike)) { @@ -140,6 +127,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State render() { const { branchLikes, currentBranchLike } = this.props; + const { configuration } = this.props.component; if (this.context.onSonarCloud && !this.context.branchesEnabled) { return null; @@ -176,18 +164,32 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State } return ( - <div - className={classNames('navbar-context-branches', 'dropdown', { - open: this.state.dropdownOpen - })}> - <a className="link-base-color link-no-underline nowrap" href="#" onClick={this.handleClick}> - <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> - <Tooltip mouseEnterDelay={1} overlay={displayName}> - <span className="text-limited text-top">{displayName}</span> - </Tooltip> - <i className="icon-dropdown little-spacer-left" /> - </a> - {this.renderDropdown()} + <div className="navbar-context-branches"> + <div className="dropdown"> + <Toggler + onRequestClose={this.closeDropdown} + open={this.state.dropdownOpen} + overlay={ + <ComponentNavBranchesMenu + branchLikes={this.props.branchLikes} + canAdmin={configuration && configuration.showSettings} + component={this.props.component} + currentBranchLike={this.props.currentBranchLike} + onClose={this.closeDropdown} + /> + }> + <a + className="link-base-color link-no-underline nowrap" + href="#" + onClick={this.handleClick}> + <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> + <Tooltip mouseEnterDelay={1} overlay={displayName}> + <span className="text-limited text-top">{displayName}</span> + </Tooltip> + <i className="icon-dropdown little-spacer-left" /> + </a> + </Toggler> + </div> {this.renderMergeBranch()} </div> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index 5772988c123..98f8de579cf 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -36,6 +36,7 @@ import { translate } from '../../../../helpers/l10n'; import { getBranchLikeUrl } from '../../../../helpers/urls'; import SearchBox from '../../../../components/controls/SearchBox'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; +import { DropdownOverlay } from '../../../../components/controls/Dropdown'; interface Props { branchLikes: BranchLike[]; @@ -51,7 +52,6 @@ interface State { } export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> { - private node?: HTMLElement | null; private listNode?: HTMLUListElement | null; private selectedBranchNode?: HTMLLIElement | null; @@ -59,13 +59,9 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, router: PropTypes.object }; - constructor(props: Props) { - super(props); - this.state = { query: '', selected: undefined }; - } + state: State = { query: '', selected: undefined }; componentDidMount() { - window.addEventListener('click', this.handleClickOutside); this.scrollToSelectedBranch(false); } @@ -73,10 +69,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, this.scrollToSelectedBranch(true); } - componentWillUnmount() { - window.removeEventListener('click', this.handleClickOutside); - } - scrollToSelectedBranch(smooth: boolean) { if (this.listNode && this.selectedBranchNode) { scrollToElement(this.selectedBranchNode, { @@ -97,12 +89,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, }); }; - handleClickOutside = (event: Event) => { - if (!this.node || !this.node.contains(event.target as HTMLElement)) { - this.props.onClose(); - } - }; - handleSearchChange = (query: string) => this.setState({ query, selected: undefined }); handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { @@ -212,7 +198,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, <React.Fragment key={getBranchLikeKey(branchLike)}> {showDivider && <li className="divider" />} {showOrphanHeader && ( - <li className="dropdown-header"> + <li className="menu-header"> <div className="display-inline-block text-middle"> {translate('branches.orphan_branches')} </div> @@ -223,12 +209,12 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, </li> )} {showPullRequestHeader && ( - <li className="dropdown-header navbar-context-meta-branch-menu-title"> + <li className="menu-header navbar-context-meta-branch-menu-title"> {translate('branches.pull_requests')} </li> )} {showShortLivingBranchHeader && ( - <li className="dropdown-header navbar-context-meta-branch-menu-title"> + <li className="menu-header navbar-context-meta-branch-menu-title"> {translate('branches.short_lived_branches')} </li> )} @@ -261,7 +247,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, component.configuration.showSettings; return ( - <div className="dropdown-menu" ref={node => (this.node = node)}> + <DropdownOverlay noPadding={true}> {this.renderSearch()} {this.renderBranchesList()} {showManageLink && ( @@ -273,7 +259,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, </Link> </div> )} - </div> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 3a78c92dc46..86ea6e3c84a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -183,19 +183,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } return ( - <Dropdown data-test="extensions"> + <Dropdown + data-test="administration" + overlay={<ul className="menu">{adminLinks}</ul>} + tagName="li"> {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: isSettingsActive || open })} - href="#" - id="component-navigation-admin" - onClick={onToggleClick}> - {translate('layout.settings')} - <i className="icon-dropdown little-spacer-left" /> - </a> - <ul className="dropdown-menu">{adminLinks}</ul> - </li> + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { active: isSettingsActive || open })} + href="#" + id="component-navigation-admin" + onClick={onToggleClick}> + {translate('layout.settings')} + <i className="icon-dropdown little-spacer-left" /> + </a> )} </Dropdown> ); @@ -421,19 +423,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } return ( - <Dropdown data-test="admin-extensions"> + <Dropdown + data-test="extensions" + overlay={<ul className="menu">{extensions.map(e => this.renderExtension(e, false))}</ul>} + tagName="li"> {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: open })} - href="#" - id="component-navigation-more" - onClick={onToggleClick}> - {translate('more')} - <i className="icon-dropdown little-spacer-left" /> - </a> - <ul className="dropdown-menu">{extensions.map(e => this.renderExtension(e, false))}</ul> - </li> + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { active: open })} + href="#" + id="component-navigation-more" + onClick={onToggleClick}> + {translate('more')} + <i className="icon-dropdown little-spacer-left" /> + </a> )} </Dropdown> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 8ea67da8b97..83a894a3b38 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -99,9 +99,9 @@ it('opens menu', () => { />, { context: { branchesEnabled: true } } ); - expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); + expect(wrapper.find('Toggler').prop('open')).toBe(false); click(wrapper.find('a')); - expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); + expect(wrapper.find('Toggler').prop('open')).toBe(true); }); it('renders single branch popup', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index 4ac93c4fb00..a7b7bb95953 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -41,8 +41,8 @@ it('should work with extensions', () => { const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { context: { branchesEnabled: true } }); - expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot(); - expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); }); it('should work with multiple extensions', () => { @@ -60,8 +60,8 @@ it('should work with multiple extensions', () => { const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { context: { branchesEnabled: true } }); - expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot(); - expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); }); it('should work for short-living branches', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index dc23c7b4368..3d9fee6a289 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -2,36 +2,71 @@ exports[`renders main branch 1`] = ` <div - className="navbar-context-branches dropdown" + className="navbar-context-branches" > - <a - className="link-base-color link-no-underline nowrap" - href="#" - onClick={[Function]} + <div + className="dropdown" > - <BranchIcon - branchLike={ - Object { - "isMain": true, - "name": "master", - } + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <ComponentNavBranchesMenu + branchLikes={ + Array [ + Object { + "isMain": true, + "name": "master", + }, + Object { + "isMain": false, + "name": "foo", + "type": "LONG", + }, + ] + } + canAdmin={undefined} + component={Object {}} + currentBranchLike={ + Object { + "isMain": true, + "name": "master", + } + } + onClose={[Function]} + /> } - className="little-spacer-right" - /> - <Tooltip - mouseEnterDelay={1} - overlay="master" > - <span - className="text-limited text-top" + <a + className="link-base-color link-no-underline nowrap" + href="#" + onClick={[Function]} > - master - </span> - </Tooltip> - <i - className="icon-dropdown little-spacer-left" - /> - </a> + <BranchIcon + branchLike={ + Object { + "isMain": true, + "name": "master", + } + } + className="little-spacer-right" + /> + <Tooltip + mouseEnterDelay={1} + overlay="master" + > + <span + className="text-limited text-top" + > + master + </span> + </Tooltip> + <i + className="icon-dropdown little-spacer-left" + /> + </a> + </Toggler> + </div> </div> `; @@ -49,39 +84,80 @@ exports[`renders no branch support popup 1`] = ` exports[`renders pull request 1`] = ` <div - className="navbar-context-branches dropdown" + className="navbar-context-branches" > - <a - className="link-base-color link-no-underline nowrap" - href="#" - onClick={[Function]} + <div + className="dropdown" > - <BranchIcon - branchLike={ - Object { - "base": "master", - "branch": "feature", - "key": "1234", - "title": "Feature PR", - "url": "https://example.com/pull/1234", - } + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <ComponentNavBranchesMenu + branchLikes={ + Array [ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + "url": "https://example.com/pull/1234", + }, + Object { + "isMain": false, + "name": "foo", + "type": "LONG", + }, + ] + } + canAdmin={undefined} + component={Object {}} + currentBranchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + "url": "https://example.com/pull/1234", + } + } + onClose={[Function]} + /> } - className="little-spacer-right" - /> - <Tooltip - mouseEnterDelay={1} - overlay="1234 – Feature PR" > - <span - className="text-limited text-top" + <a + className="link-base-color link-no-underline nowrap" + href="#" + onClick={[Function]} > - 1234 – Feature PR - </span> - </Tooltip> - <i - className="icon-dropdown little-spacer-left" - /> - </a> + <BranchIcon + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + "url": "https://example.com/pull/1234", + } + } + className="little-spacer-right" + /> + <Tooltip + mouseEnterDelay={1} + overlay="1234 – Feature PR" + > + <span + className="text-limited text-top" + > + 1234 – Feature PR + </span> + </Tooltip> + <i + className="icon-dropdown little-spacer-left" + /> + </a> + </Toggler> + </div> <span className="note big-spacer-left text-ellipsis flex-shrink" > @@ -105,44 +181,95 @@ exports[`renders pull request 1`] = ` exports[`renders short-living branch 1`] = ` <div - className="navbar-context-branches dropdown" + className="navbar-context-branches" > - <a - className="link-base-color link-no-underline nowrap" - href="#" - onClick={[Function]} + <div + className="dropdown" > - <BranchIcon - branchLike={ - Object { - "isMain": false, - "mergeBranch": "master", - "name": "foo", - "status": Object { - "bugs": 0, - "codeSmells": 0, - "qualityGateStatus": "OK", - "vulnerabilities": 0, - }, - "type": "SHORT", - } + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <ComponentNavBranchesMenu + branchLikes={ + Array [ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "foo", + "status": Object { + "bugs": 0, + "codeSmells": 0, + "qualityGateStatus": "OK", + "vulnerabilities": 0, + }, + "type": "SHORT", + }, + Object { + "isMain": false, + "name": "foo", + "type": "LONG", + }, + ] + } + canAdmin={undefined} + component={Object {}} + currentBranchLike={ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "foo", + "status": Object { + "bugs": 0, + "codeSmells": 0, + "qualityGateStatus": "OK", + "vulnerabilities": 0, + }, + "type": "SHORT", + } + } + onClose={[Function]} + /> } - className="little-spacer-right" - /> - <Tooltip - mouseEnterDelay={1} - overlay="foo" > - <span - className="text-limited text-top" + <a + className="link-base-color link-no-underline nowrap" + href="#" + onClick={[Function]} > - foo - </span> - </Tooltip> - <i - className="icon-dropdown little-spacer-left" - /> - </a> + <BranchIcon + branchLike={ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "foo", + "status": Object { + "bugs": 0, + "codeSmells": 0, + "qualityGateStatus": "OK", + "vulnerabilities": 0, + }, + "type": "SHORT", + } + } + className="little-spacer-right" + /> + <Tooltip + mouseEnterDelay={1} + overlay="foo" + > + <span + className="text-limited text-top" + > + foo + </span> + </Tooltip> + <i + className="icon-dropdown little-spacer-left" + /> + </a> + </Toggler> + </div> <span className="note big-spacer-left" > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index cac918b4985..3296a5ced18 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders list 1`] = ` -<div - className="dropdown-menu" +<DropdownOverlay + noPadding={true} > <div className="menu-search" @@ -43,7 +43,7 @@ exports[`renders list 1`] = ` key="pull-request-1234" > <li - className="dropdown-header navbar-context-meta-branch-menu-title" + className="menu-header navbar-context-meta-branch-menu-title" > branches.pull_requests </li> @@ -80,7 +80,7 @@ exports[`renders list 1`] = ` className="divider" /> <li - className="dropdown-header" + className="menu-header" > <div className="display-inline-block text-middle" @@ -181,7 +181,7 @@ exports[`renders list 1`] = ` className="divider" /> <li - className="dropdown-header" + className="menu-header" > <div className="display-inline-block text-middle" @@ -221,12 +221,12 @@ exports[`renders list 1`] = ` /> </React.Fragment> </ul> -</div> +</DropdownOverlay> `; exports[`searches 1`] = ` -<div - className="dropdown-menu" +<DropdownOverlay + noPadding={true} > <div className="menu-search" @@ -246,7 +246,7 @@ exports[`searches 1`] = ` key="branch-foobar" > <li - className="dropdown-header navbar-context-meta-branch-menu-title" + className="menu-header navbar-context-meta-branch-menu-title" > branches.short_lived_branches </li> @@ -303,5 +303,5 @@ exports[`searches 1`] = ` /> </React.Fragment> </ul> -</div> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 249f20e49dc..3abe30c25f4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -90,7 +90,82 @@ exports[`should work for all qualifiers 1`] = ` </Link> </li> <Dropdown - data-test="extensions" + data-test="administration" + overlay={ + <ul + className="menu" + > + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, + } + } + > + project_settings.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/branches", + "query": Object { + "id": "foo", + }, + } + } + > + project_branches.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/webhooks", + "query": Object { + "id": "foo", + }, + } + } + > + webhooks.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, + } + } + > + deletion.page + </Link> + </li> + </ul> + } + tagName="li" /> </NavBarTabs> `; @@ -185,7 +260,31 @@ exports[`should work for all qualifiers 2`] = ` </Link> </li> <Dropdown - data-test="extensions" + data-test="administration" + overlay={ + <ul + className="menu" + > + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, + } + } + > + project_settings.page + </Link> + </li> + </ul> + } + tagName="li" /> </NavBarTabs> `; @@ -280,7 +379,31 @@ exports[`should work for all qualifiers 3`] = ` </Link> </li> <Dropdown - data-test="extensions" + data-test="administration" + overlay={ + <ul + className="menu" + > + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, + } + } + > + deletion.page + </Link> + </li> + </ul> + } + tagName="li" /> </NavBarTabs> `; @@ -467,7 +590,31 @@ exports[`should work for all qualifiers 5`] = ` </Link> </li> <Dropdown - data-test="extensions" + data-test="administration" + overlay={ + <ul + className="menu" + > + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, + } + } + > + deletion.page + </Link> + </li> + </ul> + } + tagName="li" /> </NavBarTabs> `; @@ -710,355 +857,291 @@ exports[`should work for short-living branches 1`] = ` `; exports[`should work with extensions 1`] = ` -<li - className="dropdown" -> - <a - className="dropdown-toggle" - href="#" - id="component-navigation-admin" - onClick={[Function]} - > - layout.settings - <i - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="settings" +<Dropdown + data-test="extensions" + overlay={ + <ul + className="menu" > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-foo", + "query": Object { + "id": "foo", + }, + } } - } - > - project_settings.page - </Link> - </li> - <li - key="branches" + > + ComponentFoo + </Link> + </li> + </ul> + } + tagName="li" +/> +`; + +exports[`should work with extensions 2`] = ` +<Dropdown + data-test="administration" + overlay={ + <ul + className="menu" > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, + } } - } - > - project_branches.page - </Link> - </li> - <li - key="webhooks" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, + > + project_settings.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/branches", + "query": Object { + "id": "foo", + }, + } } - } - > - webhooks.page - </Link> - </li> - <li - key="foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - }, + > + project_branches.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/webhooks", + "query": Object { + "id": "foo", + }, + } } - } - > - Foo - </Link> - </li> - <li - key="project_delete" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + > + webhooks.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/foo", + "query": Object { + "id": "foo", + }, + } } - } - > - deletion.page - </Link> - </li> - </ul> -</li> -`; - -exports[`should work with extensions 2`] = ` -<li - className="dropdown" -> - <a - className="dropdown-toggle" - href="#" - id="component-navigation-more" - onClick={[Function]} - > - more - <i - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="component-foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - }, + > + Foo + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, + } } - } - > - ComponentFoo - </Link> - </li> - </ul> -</li> + > + deletion.page + </Link> + </li> + </ul> + } + tagName="li" +/> `; exports[`should work with multiple extensions 1`] = ` -<li - className="dropdown" -> - <a - className="dropdown-toggle" - href="#" - id="component-navigation-admin" - onClick={[Function]} - > - layout.settings - <i - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="settings" +<Dropdown + data-test="extensions" + overlay={ + <ul + className="menu" > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-foo", + "query": Object { + "id": "foo", + }, + } } - } - > - project_settings.page - </Link> - </li> - <li - key="branches" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, + > + ComponentFoo + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-bar", + "query": Object { + "id": "foo", + }, + } } - } - > - project_branches.page - </Link> - </li> - <li - key="webhooks" + > + ComponentBar + </Link> + </li> + </ul> + } + tagName="li" +/> +`; + +exports[`should work with multiple extensions 2`] = ` +<Dropdown + data-test="administration" + overlay={ + <ul + className="menu" > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, + } } - } - > - webhooks.page - </Link> - </li> - <li - key="foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - }, + > + project_settings.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/branches", + "query": Object { + "id": "foo", + }, + } } - } - > - Foo - </Link> - </li> - <li - key="bar" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/bar", - "query": Object { - "id": "foo", - }, + > + project_branches.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/webhooks", + "query": Object { + "id": "foo", + }, + } } - } - > - Bar - </Link> - </li> - <li - key="project_delete" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, + > + webhooks.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/foo", + "query": Object { + "id": "foo", + }, + } } - } - > - deletion.page - </Link> - </li> - </ul> -</li> -`; - -exports[`should work with multiple extensions 2`] = ` -<li - className="dropdown" -> - <a - className="dropdown-toggle" - href="#" - id="component-navigation-more" - onClick={[Function]} - > - more - <i - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="component-foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - }, + > + Foo + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/bar", + "query": Object { + "id": "foo", + }, + } } - } - > - ComponentFoo - </Link> - </li> - <li - key="component-bar" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-bar", - "query": Object { - "id": "foo", - }, + > + Bar + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, + } } - } - > - ComponentBar - </Link> - </li> - </ul> -</li> + > + deletion.page + </Link> + </li> + </ul> + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 866edfae805..9e1eabf9be6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -99,14 +99,12 @@ class GlobalNav extends React.PureComponent<Props, State> { <ul className="global-navbar-menu pull-right"> <GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} /> - <li> - <EmbedDocsPopupHelper - currentUser={this.props.currentUser} - showTooltip={this.state.onboardingTutorialTooltip} - suggestions={this.props.suggestions} - tooltip={!this.props.onSonarCloud} - /> - </li> + <EmbedDocsPopupHelper + currentUser={this.props.currentUser} + showTooltip={this.state.onboardingTutorialTooltip} + suggestions={this.props.suggestions} + tooltip={!this.props.onSonarCloud} + /> <Search appState={this.props.appState} currentUser={this.props.currentUser} /> {isLoggedIn(this.props.currentUser) && this.props.onSonarCloud && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index 443f074225c..cab30c446ae 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -153,19 +153,20 @@ export default class GlobalNavMenu extends React.PureComponent<Props> { return null; } return ( - <Dropdown> + <Dropdown + overlay={<ul className="menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>} + tagName="li"> {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: open })} - href="#" - id="global-navigation-more" - onClick={onToggleClick}> - {translate('more')} - <span className="icon-dropdown little-spacer-left" /> - </a> - <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul> - </li> + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { active: open })} + href="#" + id="global-navigation-more" + onClick={onToggleClick}> + {translate('more')} + <span className="icon-dropdown little-spacer-left" /> + </a> )} </Dropdown> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 1ae29cfc8ab..d58fb2379ac 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm'; import PlusIcon from '../../../../components/icons-components/PlusIcon'; @@ -65,35 +64,41 @@ export default class GlobalNavPlus extends React.PureComponent<Props, State> { render() { return ( - <Dropdown> + <Dropdown + overlay={ + <ul className="menu"> + <li> + <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> + {translate('my_account.analyze_new_project')} + </a> + </li> + <li className="divider" /> + <li> + <a className="js-new-organization" href="#" onClick={this.handleNewOrganizationClick}> + {translate('my_account.create_new_organization')} + </a> + </li> + </ul> + } + tagName="li"> {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a className="navbar-plus" href="#" onClick={onToggleClick}> + <> + <a + aria-expanded={String(open)} + aria-haspopup="true" + className="navbar-plus" + href="#" + onClick={onToggleClick}> <PlusIcon /> </a> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> - {translate('my_account.analyze_new_project')} - </a> - </li> - <li className="divider" /> - <li> - <a - className="js-new-organization" - href="#" - onClick={this.handleNewOrganizationClick}> - {translate('my_account.create_new_organization')} - </a> - </li> - </ul> + {this.state.createOrganization && ( <CreateOrganizationForm onClose={this.closeCreateOrganizationForm} onCreate={this.handleCreateOrganization} /> )} - </li> + </> )} </Dropdown> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index b568f713458..af85d305412 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { sortBy } from 'lodash'; import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; @@ -63,52 +62,52 @@ export default class GlobalNavUser extends React.PureComponent<Props> { const currentUser = this.props.currentUser as LoggedInUser; const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', 'js-user-authenticated', { open })}> - <a className="dropdown-toggle navbar-avatar" href="#" onClick={onToggleClick}> - <Avatar - hash={currentUser.avatar} - name={currentUser.name} - size={theme.globalNavContentHeightRaw} - /> - </a> - <ul className="dropdown-menu dropdown-menu-right"> - <li className="dropdown-item"> - <div className="text-ellipsis text-muted" title={currentUser.name}> - <strong>{currentUser.name}</strong> + <Dropdown + className="js-user-authenticated" + overlay={ + <ul className="menu"> + <li className="menu-item"> + <div className="text-ellipsis text-muted" title={currentUser.name}> + <strong>{currentUser.name}</strong> + </div> + {currentUser.email != null && ( + <div + className="little-spacer-top text-ellipsis text-muted" + title={currentUser.email}> + {currentUser.email} </div> - {currentUser.email != null && ( - <div - className="little-spacer-top text-ellipsis text-muted" - title={currentUser.email}> - {currentUser.email} - </div> - )} - </li> - <li className="divider" /> - <li> - <Link to="/account">{translate('my_account.page')}</Link> - </li> - {hasOrganizations && <li role="separator" className="divider" />} - {hasOrganizations && ( - <li> - <Link to="/account/organizations">{translate('my_organizations')}</Link> - </li> )} - {hasOrganizations && - sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( - <OrganizationListItem key={organization.key} organization={organization} /> - ))} - {hasOrganizations && <li role="separator" className="divider" />} + </li> + <li className="divider" /> + <li> + <Link to="/account">{translate('my_account.page')}</Link> + </li> + {hasOrganizations && <li className="divider" role="separator" />} + {hasOrganizations && ( <li> - <a onClick={this.handleLogout} href="#"> - {translate('layout.logout')} - </a> + <Link to="/account/organizations">{translate('my_organizations')}</Link> </li> - </ul> - </li> - )} + )} + {hasOrganizations && + sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + {hasOrganizations && <li className="divider" role="separator" />} + <li> + <a href="#" onClick={this.handleLogout}> + {translate('layout.logout')} + </a> + </li> + </ul> + } + tagName="li"> + <a className="dropdown-toggle navbar-avatar" href="#"> + <Avatar + hash={currentUser.avatar} + name={currentUser.name} + size={theme.globalNavContentHeightRaw} + /> + </a> </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 22fa861f34f..ba791499f59 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -32,7 +32,7 @@ it('should work with extensions', () => { const wrapper = shallow( <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} /> ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should show administration menu if the user has the rights', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx index 1e02139f691..3e4110f5193 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx @@ -25,14 +25,16 @@ import { click } from '../../../../../helpers/testUtils'; it('render', () => { const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={jest.fn()} />); expect(wrapper.is('Dropdown')).toBe(true); - expect(wrapper.find('Dropdown').shallow()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('opens onboarding', () => { const openOnboardingTutorial = jest.fn(); - const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />) - .find('Dropdown') - .shallow(); + const wrapper = shallow( + shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />) + .find('Dropdown') + .prop('overlay') + ); click(wrapper.find('.js-new-project')); expect(openOnboardingTutorial).toBeCalled(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index ec18ecba3a9..152efb5d69c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => { <GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} /> ); wrapper.setState({ open: true }); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should render user organizations', () => { @@ -51,7 +51,7 @@ it('should render user organizations', () => { <GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} /> ); wrapper.setState({ open: true }); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should not render user organizations when they are not activated', () => { @@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => { /> ); wrapper.setState({ open: true }); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap index 27714386573..6eb526f021f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap @@ -77,34 +77,22 @@ exports[`should show administration menu if the user has the rights 1`] = ` `; exports[`should work with extensions 1`] = ` -<li - className="dropdown" -> - <a - className="dropdown-toggle" - href="#" - id="global-navigation-more" - onClick={[Function]} - > - more - <span - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="foo" +<Dropdown + overlay={ + <ul + className="menu" > - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/extension/foo" - > - Foo - </Link> - </li> - </ul> -</li> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/extension/foo" + > + Foo + </Link> + </li> + </ul> + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap index 4400a02b250..1664c15d58c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap @@ -1,40 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`render 1`] = ` -<li - className="dropdown" -> - <a - className="navbar-plus" - href="#" - onClick={[Function]} - > - <PlusIcon /> - </a> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li> - <a - className="js-new-project" - href="#" - onClick={[Function]} - > - my_account.analyze_new_project - </a> - </li> - <li - className="divider" - /> - <li> - <a - className="js-new-organization" - href="#" - onClick={[Function]} - > - my_account.create_new_organization - </a> - </li> - </ul> -</li> +<Dropdown + overlay={ + <ul + className="menu" + > + <li> + <a + className="js-new-project" + href="#" + onClick={[Function]} + > + my_account.analyze_new_project + </a> + </li> + <li + className="divider" + /> + <li> + <a + className="js-new-organization" + href="#" + onClick={[Function]} + > + my_account.create_new_organization + </a> + </li> + </ul> + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap index e83123dfbdc..316ff4a8e44 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap @@ -1,13 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should not render user organizations when they are not activated 1`] = ` -<li - className="dropdown js-user-authenticated" +<Dropdown + className="js-user-authenticated" + overlay={ + <ul + className="menu" + > + <li + className="menu-item" + > + <div + className="text-ellipsis text-muted" + title="foo" + > + <strong> + foo + </strong> + </div> + <div + className="little-spacer-top text-ellipsis text-muted" + title="foo@bar.baz" + > + foo@bar.baz + </div> + </li> + <li + className="divider" + /> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/account" + > + my_account.page + </Link> + </li> + <li> + <a + href="#" + onClick={[Function]} + > + layout.logout + </a> + </li> + </ul> + } + tagName="li" > <a className="dropdown-toggle navbar-avatar" href="#" - onClick={[Function]} > <Connect(Avatar) hash="abcd1234" @@ -15,49 +59,7 @@ exports[`should not render user organizations when they are not activated 1`] = size={32} /> </a> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li - className="dropdown-item" - > - <div - className="text-ellipsis text-muted" - title="foo" - > - <strong> - foo - </strong> - </div> - <div - className="little-spacer-top text-ellipsis text-muted" - title="foo@bar.baz" - > - foo@bar.baz - </div> - </li> - <li - className="divider" - /> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/account" - > - my_account.page - </Link> - </li> - <li> - <a - href="#" - onClick={[Function]} - > - layout.logout - </a> - </li> - </ul> -</li> +</Dropdown> `; exports[`should render the right interface for anonymous user 1`] = ` @@ -73,13 +75,57 @@ exports[`should render the right interface for anonymous user 1`] = ` `; exports[`should render the right interface for logged in user 1`] = ` -<li - className="dropdown js-user-authenticated" +<Dropdown + className="js-user-authenticated" + overlay={ + <ul + className="menu" + > + <li + className="menu-item" + > + <div + className="text-ellipsis text-muted" + title="foo" + > + <strong> + foo + </strong> + </div> + <div + className="little-spacer-top text-ellipsis text-muted" + title="foo@bar.baz" + > + foo@bar.baz + </div> + </li> + <li + className="divider" + /> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/account" + > + my_account.page + </Link> + </li> + <li> + <a + href="#" + onClick={[Function]} + > + layout.logout + </a> + </li> + </ul> + } + tagName="li" > <a className="dropdown-toggle navbar-avatar" href="#" - onClick={[Function]} > <Connect(Avatar) hash="abcd1234" @@ -87,59 +133,105 @@ exports[`should render the right interface for logged in user 1`] = ` size={32} /> </a> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li - className="dropdown-item" - > - <div - className="text-ellipsis text-muted" - title="foo" - > - <strong> - foo - </strong> - </div> - <div - className="little-spacer-top text-ellipsis text-muted" - title="foo@bar.baz" - > - foo@bar.baz - </div> - </li> - <li - className="divider" - /> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/account" - > - my_account.page - </Link> - </li> - <li> - <a - href="#" - onClick={[Function]} - > - layout.logout - </a> - </li> - </ul> -</li> +</Dropdown> `; exports[`should render user organizations 1`] = ` -<li - className="dropdown js-user-authenticated" +<Dropdown + className="js-user-authenticated" + overlay={ + <ul + className="menu" + > + <li + className="menu-item" + > + <div + className="text-ellipsis text-muted" + title="foo" + > + <strong> + foo + </strong> + </div> + <div + className="little-spacer-top text-ellipsis text-muted" + title="foo@bar.baz" + > + foo@bar.baz + </div> + </li> + <li + className="divider" + /> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/account" + > + my_account.page + </Link> + </li> + <li + className="divider" + role="separator" + /> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/account/organizations" + > + my_organizations + </Link> + </li> + <OrganizationListItem + organization={ + Object { + "key": "bar", + "name": "bar", + "projectVisibility": "public", + } + } + /> + <OrganizationListItem + organization={ + Object { + "key": "foo", + "name": "Foo", + "projectVisibility": "public", + } + } + /> + <OrganizationListItem + organization={ + Object { + "key": "myorg", + "name": "MyOrg", + "projectVisibility": "public", + } + } + /> + <li + className="divider" + role="separator" + /> + <li> + <a + href="#" + onClick={[Function]} + > + layout.logout + </a> + </li> + </ul> + } + tagName="li" > <a className="dropdown-toggle navbar-avatar" href="#" - onClick={[Function]} > <Connect(Avatar) hash="abcd1234" @@ -147,94 +239,5 @@ exports[`should render user organizations 1`] = ` size={32} /> </a> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li - className="dropdown-item" - > - <div - className="text-ellipsis text-muted" - title="foo" - > - <strong> - foo - </strong> - </div> - <div - className="little-spacer-top text-ellipsis text-muted" - title="foo@bar.baz" - > - foo@bar.baz - </div> - </li> - <li - className="divider" - /> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/account" - > - my_account.page - </Link> - </li> - <li - className="divider" - role="separator" - /> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/account/organizations" - > - my_organizations - </Link> - </li> - <OrganizationListItem - key="bar" - organization={ - Object { - "key": "bar", - "name": "bar", - "projectVisibility": "public", - } - } - /> - <OrganizationListItem - key="foo" - organization={ - Object { - "key": "foo", - "name": "Foo", - "projectVisibility": "public", - } - } - /> - <OrganizationListItem - key="myorg" - organization={ - Object { - "key": "myorg", - "name": "MyOrg", - "projectVisibility": "public", - } - } - /> - <li - className="divider" - role="separator" - /> - <li> - <a - href="#" - onClick={[Function]} - > - layout.logout - </a> - </li> - </ul> -</li> +</Dropdown> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 8379d1822bb..ff656a311a5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -91,51 +91,54 @@ export default class SettingsNav extends React.PureComponent<Props> { extension => extension.key !== 'license/support' ); return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { - active: - open || - (!this.isSecurityActive() && - !this.isProjectsActive() && - !this.isSystemActive() && - !this.isSomethingActive(['/admin/extension/license/support']) && - !this.isMarketplace()) - })} - href="#" - id="settings-navigation-configuration" - onClick={onToggleClick}> - {translate('sidebar.project_settings')} - <i className="icon-dropdown little-spacer-left" /> - </a> - <ul className="dropdown-menu"> - <li> - <IndexLink activeClassName="active" to="/admin/settings"> - {translate('settings.page')} - </IndexLink> - </li> - <li> - <IndexLink activeClassName="active" to="/admin/settings/encryption"> - {translate('property.category.security.encryption')} - </IndexLink> - </li> + <Dropdown + overlay={ + <ul className="menu"> + <li> + <IndexLink activeClassName="active" to="/admin/settings"> + {translate('settings.page')} + </IndexLink> + </li> + <li> + <IndexLink activeClassName="active" to="/admin/settings/encryption"> + {translate('property.category.security.encryption')} + </IndexLink> + </li> + <li> + <IndexLink activeClassName="active" to="/admin/custom_metrics"> + {translate('custom_metrics.page')} + </IndexLink> + </li> + {!organizationsEnabled && ( <li> - <IndexLink activeClassName="active" to="/admin/custom_metrics"> - {translate('custom_metrics.page')} + <IndexLink activeClassName="active" to="/admin/webhooks"> + {translate('webhooks.page')} </IndexLink> </li> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/webhooks"> - {translate('webhooks.page')} - </IndexLink> - </li> - )} - {extensionsWithoutSupport.map(this.renderExtension)} - </ul> - </li> + )} + {extensionsWithoutSupport.map(this.renderExtension)} + </ul> + } + tagName="li"> + {({ onToggleClick, open }) => ( + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { + active: + open || + (!this.isSecurityActive() && + !this.isProjectsActive() && + !this.isSystemActive() && + !this.isSomethingActive(['/admin/extension/license/support']) && + !this.isMarketplace()) + })} + href="#" + id="settings-navigation-configuration" + onClick={onToggleClick}> + {translate('sidebar.project_settings')} + <i className="icon-dropdown little-spacer-left" /> + </a> )} </Dropdown> ); @@ -144,30 +147,33 @@ export default class SettingsNav extends React.PureComponent<Props> { renderProjectsTab() { const { organizationsEnabled } = this.props; return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: open || this.isProjectsActive() })} - href="#" - onClick={onToggleClick}> - {translate('sidebar.projects')} <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu"> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/projects_management"> - {translate('management')} - </IndexLink> - </li> - )} + <Dropdown + overlay={ + <ul className="menu"> + {!organizationsEnabled && ( <li> - <IndexLink activeClassName="active" to="/admin/background_tasks"> - {translate('background_tasks.page')} + <IndexLink activeClassName="active" to="/admin/projects_management"> + {translate('management')} </IndexLink> </li> - </ul> - </li> + )} + <li> + <IndexLink activeClassName="active" to="/admin/background_tasks"> + {translate('background_tasks.page')} + </IndexLink> + </li> + </ul> + } + tagName="li"> + {({ onToggleClick, open }) => ( + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { active: open || this.isProjectsActive() })} + href="#" + onClick={onToggleClick}> + {translate('sidebar.projects')} <i className="icon-dropdown" /> + </a> )} </Dropdown> ); @@ -176,44 +182,47 @@ export default class SettingsNav extends React.PureComponent<Props> { renderSecurityTab() { const { organizationsEnabled } = this.props; return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: open || this.isSecurityActive() })} - href="#" - onClick={onToggleClick}> - {translate('sidebar.security')} <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu"> + <Dropdown + overlay={ + <ul className="menu"> + <li> + <IndexLink activeClassName="active" to="/admin/users"> + {translate('users.page')} + </IndexLink> + </li> + {!organizationsEnabled && ( <li> - <IndexLink activeClassName="active" to="/admin/users"> - {translate('users.page')} + <IndexLink activeClassName="active" to="/admin/groups"> + {translate('user_groups.page')} </IndexLink> </li> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/groups"> - {translate('user_groups.page')} - </IndexLink> - </li> - )} - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/permissions"> - {translate('global_permissions.page')} - </IndexLink> - </li> - )} - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/permission_templates"> - {translate('permission_templates')} - </IndexLink> - </li> - )} - </ul> - </li> + )} + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/permissions"> + {translate('global_permissions.page')} + </IndexLink> + </li> + )} + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/permission_templates"> + {translate('permission_templates')} + </IndexLink> + </li> + )} + </ul> + } + tagName="li"> + {({ onToggleClick, open }) => ( + <a + aria-expanded={String(open)} + aria-haspopup="true" + className={classNames('dropdown-toggle', { active: open || this.isSecurityActive() })} + href="#" + onClick={onToggleClick}> + {translate('sidebar.security')} <i className="icon-dropdown" /> + </a> )} </Dropdown> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index ae3f5e900af..256662531cb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -33,5 +33,5 @@ it('should work with extensions', () => { /> ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('Dropdown').map(x => x.dive())).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index d7495bbbfc3..29a88938c4b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -14,9 +14,123 @@ exports[`should work with extensions 1`] = ` </h1> </header> <NavBarTabs> - <Dropdown /> - <Dropdown /> - <Dropdown /> + <Dropdown + overlay={ + <ul + className="menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/settings" + > + settings.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/settings/encryption" + > + property.category.security.encryption + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/custom_metrics" + > + custom_metrics.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/webhooks" + > + webhooks.page + </IndexLink> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/admin/extension/foo" + > + Foo + </Link> + </li> + </ul> + } + tagName="li" + /> + <Dropdown + overlay={ + <ul + className="menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/users" + > + users.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/groups" + > + user_groups.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permissions" + > + global_permissions.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permission_templates" + > + permission_templates + </IndexLink> + </li> + </ul> + } + tagName="li" + /> + <Dropdown + overlay={ + <ul + className="menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/projects_management" + > + management + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/background_tasks" + > + background_tasks.page + </IndexLink> + </li> + </ul> + } + tagName="li" + /> <li> <IndexLink activeClassName="active" @@ -39,154 +153,122 @@ exports[`should work with extensions 1`] = ` exports[`should work with extensions 2`] = ` Array [ - <li - className="dropdown" - > - <a - className="dropdown-toggle active" - href="#" - id="settings-navigation-configuration" - onClick={[Function]} - > - sidebar.project_settings - <i - className="icon-dropdown little-spacer-left" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/settings" - > - settings.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/settings/encryption" - > - property.category.security.encryption - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/custom_metrics" - > - custom_metrics.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/webhooks" - > - webhooks.page - </IndexLink> - </li> - <li - key="foo" + <Dropdown + overlay={ + <ul + className="menu" > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/admin/extension/foo" - > - Foo - </Link> - </li> - </ul> - </li>, - <li - className="dropdown" - > - <a - className="dropdown-toggle" - href="#" - onClick={[Function]} - > - sidebar.security - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/users" - > - users.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/groups" - > - user_groups.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/permissions" - > - global_permissions.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/permission_templates" - > - permission_templates - </IndexLink> - </li> - </ul> - </li>, - <li - className="dropdown" - > - <a - className="dropdown-toggle" - href="#" - onClick={[Function]} - > - sidebar.projects - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/projects_management" - > - management - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/background_tasks" - > - background_tasks.page - </IndexLink> - </li> - </ul> - </li>, + <li> + <IndexLink + activeClassName="active" + to="/admin/settings" + > + settings.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/settings/encryption" + > + property.category.security.encryption + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/custom_metrics" + > + custom_metrics.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/webhooks" + > + webhooks.page + </IndexLink> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/admin/extension/foo" + > + Foo + </Link> + </li> + </ul> + } + tagName="li" + />, + <Dropdown + overlay={ + <ul + className="menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/users" + > + users.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/groups" + > + user_groups.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permissions" + > + global_permissions.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permission_templates" + > + permission_templates + </IndexLink> + </li> + </ul> + } + tagName="li" + />, + <Dropdown + overlay={ + <ul + className="menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/projects_management" + > + management + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/background_tasks" + > + background_tasks.page + </IndexLink> + </li> + </ul> + } + tagName="li" + />, ] `; diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js index 5912990d256..327844759f8 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.js +++ b/server/sonar-web/src/main/js/app/components/search/Search.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import key from 'keymaster'; import { debounce, keyBy, uniqBy } from 'lodash'; import { FormattedMessage } from 'react-intl'; @@ -31,12 +30,14 @@ import { sortQualifiers } from './utils'; import RecentHistory from '../../components/RecentHistory'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import ClockIcon from '../../../components/common/ClockIcon'; +import OutsideClickHandler from '../../../components/controls/OutsideClickHandler'; import SearchBox from '../../../components/controls/SearchBox'; import { getSuggestions } from '../../../api/components'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import { getProjectUrl } from '../../../helpers/urls'; import './Search.css'; +import { DropdownOverlay } from '../../../components/controls/Dropdown'; /*:: type Props = {| @@ -115,17 +116,22 @@ export default class Search extends React.PureComponent { componentWillUnmount() { this.mounted = false; key.unbind('s'); - window.removeEventListener('click', this.handleClickOutside); } - handleClickOutside = (event /*: { target: HTMLElement } */) => { - if (!this.node || !this.node.contains(event.target)) { - this.closeSearch(false); + handleClickOutside = () => { + this.closeSearch(false); + }; + + handleFocus = () => { + // simulate click to close any other dropdowns + const body = document.documentElement; + if (body) { + body.click(); } + this.openSearch(); }; openSearch = () => { - window.addEventListener('click', this.handleClickOutside); if (!this.state.open && !this.state.query) { this.search(''); } @@ -136,7 +142,6 @@ export default class Search extends React.PureComponent { if (this.input) { this.input.blur(); } - window.removeEventListener('click', this.handleClickOutside); this.setState( clear ? { @@ -304,10 +309,6 @@ export default class Search extends React.PureComponent { this.setState({ selected }); }; - handleClick = (event /*: Event */) => { - event.stopPropagation(); - }; - innerRef = (component /*: string */, node /*: HTMLElement */) => { this.nodes[component] = node; }; @@ -337,61 +338,65 @@ export default class Search extends React.PureComponent { ); render() { - const dropdownClassName = classNames('dropdown', 'navbar-search', { open: this.state.open }); - - return ( - <li className={dropdownClassName}> + const search = ( + <li className="navbar-search dropdown"> <DeferredSpinner className="navbar-search-icon" loading={this.state.loading} /> <SearchBox + autoFocus={this.state.open} innerRef={this.searchInputRef} minLength={2} onChange={this.handleQueryChange} - onClick={this.handleClick} - onFocus={this.openSearch} + onFocus={this.handleFocus} onKeyDown={this.handleKeyDown} placeholder={translate('search.placeholder')} value={this.state.query} /> {this.state.shortQuery && ( - <span className={classNames('navbar-search-input-hint')}> + <span className="navbar-search-input-hint"> {translateWithParameters('select2.tooShort', 2)} </span> )} {this.state.open && Object.keys(this.state.results).length > 0 && ( - <div - className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown" - ref={node => (this.node = node)}> - <SearchResults - allowMore={this.state.query.length !== 1} - loadingMore={this.state.loadingMore} - more={this.state.more} - onMoreClick={this.searchMore} - onSelect={this.handleSelect} - renderNoResults={this.renderNoResults} - renderResult={this.renderResult} - results={this.state.results} - selected={this.state.selected} - /> - <div className="dropdown-bottom-hint"> - <div className="pull-right"> - <ClockIcon className="little-spacer-right" size={12} /> - {translate('recently_browsed')} - </div> - <FormattedMessage - defaultMessage={translate('search.shortcut_hint')} - id="search.shortcut_hint" - values={{ - shortcut: <span className="shortcut-button shortcut-button-small">s</span> - }} + <DropdownOverlay noPadding={true}> + <div className="global-navbar-search-dropdown" ref={node => (this.node = node)}> + <SearchResults + allowMore={this.state.query.length !== 1} + loadingMore={this.state.loadingMore} + more={this.state.more} + onMoreClick={this.searchMore} + onSelect={this.handleSelect} + renderNoResults={this.renderNoResults} + renderResult={this.renderResult} + results={this.state.results} + selected={this.state.selected} /> + <div className="dropdown-bottom-hint"> + <div className="pull-right"> + <ClockIcon className="little-spacer-right" size={12} /> + {translate('recently_browsed')} + </div> + <FormattedMessage + defaultMessage={translate('search.shortcut_hint')} + id="search.shortcut_hint" + values={{ + shortcut: <span className="shortcut-button shortcut-button-small">s</span> + }} + /> + </div> </div> - </div> + </DropdownOverlay> )} </li> ); + + return this.state.open ? ( + <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler> + ) : ( + search + ); } } diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.js b/server/sonar-web/src/main/js/app/components/search/SearchResults.js index c22b619639d..2d8e3b7532f 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResults.js +++ b/server/sonar-web/src/main/js/app/components/search/SearchResults.js @@ -49,12 +49,12 @@ export default class SearchResults extends React.PureComponent { const components = this.props.results[qualifier]; if (components.length > 0 && renderedComponents.length > 0) { - renderedComponents.push(<li key={`divider-${qualifier}`} className="divider" />); + renderedComponents.push(<li className="divider" key={`divider-${qualifier}`} />); } if (components.length > 0) { renderedComponents.push( - <li key={`header-${qualifier}`} className="dropdown-header"> + <li className="menu-header" key={`header-${qualifier}`}> {translate('qualifiers', qualifier)} </li> ); diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js index 2e300a88cda..01168e7a49b 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js @@ -94,13 +94,3 @@ it('shows warning about short input', () => { form.setState({ query: 'foobar x' }); expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); }); - -it('closes on click outside', () => { - const form = mount( - <Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} /> - ); - form.instance().openSearch(); - expect(form.state().open).toBe(true); - clickOutside(); - expect(form.state().open).toBe(false); -}); diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap index 432b5562168..f13cf142896 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap @@ -5,7 +5,7 @@ exports[`renders "Show More" link 1`] = ` className="menu" > <li - className="dropdown-header" + className="menu-header" key="header-TRK" > qualifiers.TRK @@ -34,7 +34,7 @@ exports[`renders "Show More" link 1`] = ` key="divider-BRC" /> <li - className="dropdown-header" + className="menu-header" key="header-BRC" > qualifiers.BRC @@ -57,7 +57,7 @@ exports[`renders different components and dividers between them 1`] = ` className="menu" > <li - className="dropdown-header" + className="menu-header" key="header-TRK" > qualifiers.TRK @@ -77,7 +77,7 @@ exports[`renders different components and dividers between them 1`] = ` key="divider-BRC" /> <li - className="dropdown-header" + className="menu-header" key="header-BRC" > qualifiers.BRC @@ -97,7 +97,7 @@ exports[`renders different components and dividers between them 1`] = ` key="divider-FIL" /> <li - className="dropdown-header" + className="menu-header" key="header-FIL" > qualifiers.FIL diff --git a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css b/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css deleted file mode 100644 index 031edf980a0..00000000000 --- a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -.bubble-popup { - position: absolute; - z-index: var(--bubblePopupZIndex); - margin-top: -16px; - margin-left: 8px; - padding: 10px; - border: 1px solid var(--barBorderColor); - border-radius: 3px; - box-sizing: border-box; - background-color: #ffffff; - box-shadow: var(--defaultShadow); - cursor: default; -} - -.bubble-popup-menu { - padding: 0; -} - -.bubble-popup-arrow, -.bubble-popup-arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border: 8px solid transparent; -} - -.bubble-popup-arrow { - top: 15px; - left: -8px; - border-left-width: 0; - border-right-color: var(--barBorderColor); -} - -.bubble-popup-arrow:after { - content: ' '; - left: 1px; - bottom: -8px; - border-left-width: 0; - border-right-color: #ffffff; -} - -.bubble-popup-bottom { - margin-top: 8px; - margin-left: -16px; -} - -.bubble-popup-bottom .bubble-popup-arrow { - top: -8px; - left: 15px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: var(--barBorderColor); -} - -.bubble-popup-bottom .bubble-popup-arrow:after { - left: -8px; - bottom: -9px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: #ffffff; -} - -.bubble-popup-bottom-right { - margin-top: 8px; - margin-left: -16px; - margin-left: 0; - margin-right: -16px; -} - -.bubble-popup-bottom-right .bubble-popup-arrow { - top: -8px; - left: 15px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: var(--barBorderColor); -} - -.bubble-popup-bottom-right .bubble-popup-arrow:after { - left: -8px; - bottom: -9px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: #ffffff; -} - -.bubble-popup-bottom-right .bubble-popup-arrow { - left: auto; - right: 15px; -} - -.bubble-popup-right { - margin-left: -8px; -} - -.bubble-popup-right .bubble-popup-arrow { - right: -8px; - left: auto; - border-right-width: 0; - border-left-width: 8px; - border-left-color: var(--barBorderColor); - border-right-color: transparent; -} - -.bubble-popup-right .bubble-popup-arrow:after { - left: auto; - right: 1px; - bottom: -8px; - border-right-width: 0; - border-left-width: 8px; - border-left-color: #ffffff; - border-right-color: transparent; -} - -.bubble-popup-container { - max-width: 560px; - max-height: 300px; - padding-right: 30px; - overflow: auto; -} - -.bubble-popup-helper { - position: relative; -} - -.bubble-popup-helper:focus { - outline: none; -} - -.bubble-popup-helper-inline { - display: inline-block; -} - -.bubble-popup-title { - margin-bottom: 5px; - font-weight: 600; -} - -.bubble-popup-section { - width: 450px; - padding-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.bubble-popup-section.fluid { - width: auto; - max-width: 450px; -} - -.bubble-popup-section + .bubble-popup-section, -.bubble-popup-section + .bubble-popup-title { - margin-top: 10px; -} - -.bubble-popup-list { - margin-top: 5px; -} - -.bubble-popup-list > li { - padding: 2px 0; -} diff --git a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css index d08b64ea06b..ef8e2bd3f10 100644 --- a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css +++ b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css @@ -19,156 +19,12 @@ */ .dropdown { position: relative; -} - -.dropdown-toggle:focus { - outline: 0; -} - -.dropdown-menu { - min-width: 160px; - padding: 5px 0; - list-style: none; - font-size: var(--smallFontSize); - text-align: left; - background-color: #fff; - position: absolute; - top: 100%; - left: 0; - z-index: var(--dropdownMenuZIndex); - display: none; - float: left; - border: 1px solid var(--barBorderColor); - background-clip: padding-box; - box-shadow: var(--defaultShadow); -} - -.dropdown-menu:focus { - outline: none; -} - -.dropdown-menu > li > a, -.dropdown-menu > li > span { - display: block; - padding: 4px 16px; - line-height: 16px; - clear: both; - font-weight: normal; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dropdown-menu > li > a { - color: var(--baseFontColor); - border-bottom: none; - transition: none; -} - -.dropdown-menu > li > a.text-danger, -.dropdown-menu > li > a.text-danger:hover { - color: var(--red) !important; -} - -.dropdown-menu .divider { - height: 1px; - margin: 6px 0; - overflow: hidden; - background-color: var(--barBorderColor); -} - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - text-decoration: none; - color: var(--baseFontColor); - background-color: var(--barBackgroundColor); -} - -.dropdown-menu > .active > a, -.dropdown-menu > li > .active, -.dropdown-menu > .active > a:hover, -.dropdown-menu > li > .active:hover, -.dropdown-menu > .active > a:focus, -.dropdown-menu > li > .active:focus { - color: var(--baseFontColor); - text-decoration: none; - outline: 0; - background-color: var(--barBackgroundColor); -} - -.dropdown-menu .menu-vertically-limited { - max-height: 300px; - overflow-y: auto; -} - -.dropdown-menu .menu-footer > a > span { - border-bottom: 1px solid var(--gray80); - color: var(--secondFontColor); -} - -.dropdown-menu .menu-footer-note { - opacity: 0; - transition: opacity 0.3s ease; -} - -.dropdown-menu .menu-footer.active .menu-footer-note { - opacity: 1; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.open > .dropdown-menu { - display: block; -} - -.open > a { - outline: 0; -} - -.dropdown-menu-right { - left: auto; - right: 0; -} - -.dropdown-menu-left { - left: 0; - right: auto; -} - -.dropdown-header { - display: block; - padding: 3px 8px 5px; - font-size: var(--smallFontSize); - color: var(--secondFontColor); - white-space: nowrap; -} - -.dropdown-item { - padding: 5px 16px; -} - -.dropdown-menu .small-divider { - height: 1px; - margin: 4px 20px; - overflow: hidden; - background-color: var(--barBackgroundColor); -} - -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: 990; + display: inline-block; + vertical-align: middle; } .dropdown-bottom-hint { line-height: 16px; - margin-top: 5px; margin-bottom: -5px; padding: 5px 10px; border-top: 1px solid var(--barBorderColor); @@ -176,9 +32,3 @@ color: var(--secondFontColor); font-size: 11px; } - -.dropdown-item-flex { - display: flex !important; - justify-content: space-between; - align-items: center; -} diff --git a/server/sonar-web/src/main/js/app/styles/components/issues.css b/server/sonar-web/src/main/js/app/styles/components/issues.css index 3ff1e991902..21eb1a3066a 100644 --- a/server/sonar-web/src/main/js/app/styles/components/issues.css +++ b/server/sonar-web/src/main/js/app/styles/components/issues.css @@ -168,8 +168,7 @@ } .issue-changelog { - min-width: 450px; - max-width: 540px; + width: 450px; max-height: 320px; overflow: auto; white-space: normal; @@ -345,14 +344,9 @@ input.issue-action-options-search { .issue-comment-bubble-popup { width: 440px; - margin-left: -220px; font-size: var(--smallFontSize); } -.issue-comment-bubble-popup .bubble-popup-arrow { - left: 220px; -} - .issue-edit-comment-bubble-popup { width: 440px; font-size: var(--smallFontSize); @@ -481,7 +475,7 @@ input.issue-action-options-search { background-color: #ccc; } -.issue .menu:not(.issues-similar-issues-menu) { +.issue .menu:not(.issues-similar-issues-menu):not(.issue-changelog) { max-height: 120px; overflow: auto; } diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css index ac3fd543e5f..e5bf1c05eda 100644 --- a/server/sonar-web/src/main/js/app/styles/components/menu.css +++ b/server/sonar-web/src/main/js/app/styles/components/menu.css @@ -31,6 +31,11 @@ outline: none; } +.menu.is-container { + padding: 5px; +} + +.menu-item, .menu > li > a, .menu > li > span { display: block; @@ -85,6 +90,11 @@ background-color: var(--barBackgroundColor); } +.menu > li > a.text-danger, +.menu > li > a.text-danger:hover { + color: var(--red) !important; +} + .menu .menu-vertically-limited, .menu.menu-vertically-limited { max-height: 300px; @@ -115,7 +125,7 @@ .menu-search { position: relative; - padding: 4px 16px 0; + padding: var(--gridSize) calc(2 * var(--gridSize)) 0; } .menu-search .search-box, @@ -143,3 +153,15 @@ padding: 4px 16px; line-height: 16px; } + +.menu-header { + padding: var(--gridSize); + font-size: 12px; + color: #777; + white-space: nowrap; +} + +.menu-header:first-child, +.divider + .menu-header { + padding-top: calc(var(--gridSize) - 5px); +} diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css index 9e5a247cdd1..eaef6aa0909 100644 --- a/server/sonar-web/src/main/js/app/styles/sonar.css +++ b/server/sonar-web/src/main/js/app/styles/sonar.css @@ -29,7 +29,6 @@ @import './components/ui.css'; @import './components/spinner.css'; @import './components/global-loading.css'; -@import './components/bubble-popup.css'; @import './components/modals.css'; @import './components/alerts.css'; @import './components/issues.css'; diff --git a/server/sonar-web/src/main/js/app/styles/style.css b/server/sonar-web/src/main/js/app/styles/style.css index 4fc6c561948..61be8927c26 100644 --- a/server/sonar-web/src/main/js/app/styles/style.css +++ b/server/sonar-web/src/main/js/app/styles/style.css @@ -200,7 +200,6 @@ } .property pre, -.bubble-popup pre, .coding-rules-detail-parameter pre { display: inline-block; min-width: 100%; @@ -213,7 +212,6 @@ } .property blockquote, -.bubble-popup blockquote, .coding-rules-detail-parameter blockquote { margin-top: 10px; padding: 10px; diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 59808a135ba..e57fc075f6c 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -56,6 +56,7 @@ module.exports = { snippetFontColor: '#f0f0f0', // sizes + grid, gridSize: `${grid}px`, baseFontSize: '13px', @@ -105,5 +106,5 @@ module.exports = { modalZIndex: '6001', modalOverlayZIndex: '6000', - bubblePopupZIndex: '5000' + popupZIndex: '5000' }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx index 63d039166cc..6b47b2d704d 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import BulkChangeModal from './BulkChangeModal'; import { Query } from '../query'; import { Profile } from '../../../api/quality-profiles'; @@ -40,7 +39,6 @@ interface State { } export default class BulkChange extends React.PureComponent<Props, State> { - closeDropdown?: () => void; state: State = { modal: false }; getSelectedProfile = () => { @@ -53,36 +51,24 @@ export default class BulkChange extends React.PureComponent<Props, State> { handleActivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'activate', modal: true, profile: undefined }); }; handleActivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'activate', modal: true, profile: this.getSelectedProfile() }); }; handleDeactivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'deactivate', modal: true, profile: undefined }); }; handleDeactivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'deactivate', modal: true, profile: this.getSelectedProfile() }); }; @@ -105,45 +91,39 @@ export default class BulkChange extends React.PureComponent<Props, State> { return ( <> - <Dropdown> - {({ closeDropdown, onToggleClick, open }) => { - this.closeDropdown = closeDropdown; - return ( - <div className={classNames('pull-left dropdown', { open })}> - <Button className="js-bulk-change" onClick={onToggleClick}> - {translate('bulk_change')} - </Button> - <ul className="dropdown-menu"> + <Dropdown + className="pull-left" + overlay={ + <ul className="menu"> + <li> + <a href="#" onClick={this.handleActivateClick}> + {translate('coding_rules.activate_in')}… + </a> + </li> + {allowActivateOnProfile && + profile && ( <li> - <a href="#" onClick={this.handleActivateClick}> - {translate('coding_rules.activate_in')}… + <a href="#" onClick={this.handleActivateInProfileClick}> + {translate('coding_rules.activate_in')} <strong>{profile.name}</strong> </a> </li> - {allowActivateOnProfile && - profile && ( - <li> - <a href="#" onClick={this.handleActivateInProfileClick}> - {translate('coding_rules.activate_in')} <strong>{profile.name}</strong> - </a> - </li> - )} + )} + <li> + <a href="#" onClick={this.handleDeactivateClick}> + {translate('coding_rules.deactivate_in')}… + </a> + </li> + {allowDeactivateOnProfile && + profile && ( <li> - <a href="#" onClick={this.handleDeactivateClick}> - {translate('coding_rules.deactivate_in')}… + <a href="#" onClick={this.handleDeactivateInProfileClick}> + {translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong> </a> </li> - {allowDeactivateOnProfile && - profile && ( - <li> - <a href="#" onClick={this.handleDeactivateInProfileClick}> - {translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong> - </a> - </li> - )} - </ul> - </div> - ); - }} + )} + </ul> + }> + <Button className="js-bulk-change">{translate('bulk_change')}</Button> </Dropdown> {this.state.modal && this.state.action && ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index 16e9f0d6f65..60d51aae6bd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -31,10 +31,11 @@ import DocTooltip from '../../../components/docs/DocTooltip'; import { translate } from '../../../helpers/l10n'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SeverityHelper from '../../../components/shared/SeverityHelper'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Dropdown from '../../../components/controls/Dropdown'; import TagsList from '../../../components/tags/TagsList'; import DateFormatter from '../../../components/intl/DateFormatter'; import { Button } from '../../../components/ui/buttons'; +import { PopupPlacement } from '../../../components/ui/popups'; interface Props { canWrite: boolean | undefined; @@ -46,19 +47,7 @@ interface Props { ruleDetails: RuleDetails; } -interface State { - tagsPopup: boolean; -} - -export default class RuleDetailsMeta extends React.PureComponent<Props, State> { - state: State = { tagsPopup: false }; - - handleTagsClick = () => { - this.setState(state => ({ tagsPopup: !state.tagsPopup })); - }; - - handleTagsPopupToggle = (show: boolean) => this.setState({ tagsPopup: show }); - +export default class RuleDetailsMeta extends React.PureComponent<Props> { renderType = () => { const { ruleDetails } = this.props; return ( @@ -106,9 +95,10 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> { return ( <li className="coding-rules-detail-property" data-meta="tags"> {this.props.canWrite ? ( - <BubblePopupHelper - isOpen={this.state.tagsPopup} - popup={ + <Dropdown + closeOnClick={false} + closeOnClickOutside={true} + overlay={ <RuleDetailsTagsPopup organization={this.props.organization} setTags={this.props.onTagsChange} @@ -116,15 +106,14 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> { tags={allTags} /> } - position="bottomleft" - togglePopup={this.handleTagsPopupToggle}> - <Button className="button-link" onClick={this.handleTagsClick}> + overlayPlacement={PopupPlacement.BottomLeft}> + <Button className="button-link"> <TagsList allowUpdate={canWrite} tags={allTags.length > 0 ? allTags : [translate('coding_rules.no_tags')]} /> </Button> - </BubblePopupHelper> + </Dropdown> ) : ( <TagsList allowUpdate={canWrite} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx index 38d3520623c..72e0c7a8fa0 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx @@ -21,11 +21,9 @@ import * as React from 'react'; import { without, uniq, difference } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; import { getRuleTags } from '../../../api/rules'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; interface Props { organization: string | undefined; - popupPosition?: BubblePopupPosition; setTags: (tags: string[]) => void; sysTags: string[]; tags: string[]; @@ -81,7 +79,6 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta onSearch={this.onSearch} onSelect={this.onSelect} onUnselect={this.onUnselect} - position={this.props.popupPosition || {}} selectedTags={this.props.tags} tags={availableTags} /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx index 8340279f3ad..f7106afcc46 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { Query } from '../query'; import { Rule } from '../../../app/types'; import Dropdown from '../../../components/controls/Dropdown'; @@ -31,32 +30,21 @@ interface Props { } export default class SimilarRulesFilter extends React.PureComponent<Props> { - closeDropdown?: () => void; - handleLanguageClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.props.onFilterChange({ languages: [this.props.rule.lang] }); }; handleTypeClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.props.onFilterChange({ types: [this.props.rule.type] }); }; handleSeverityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } if (this.props.rule.severity) { this.props.onFilterChange({ severities: [this.props.rule.severity] }); } @@ -65,9 +53,6 @@ export default class SimilarRulesFilter extends React.PureComponent<Props> { handleTagClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } const { tag } = event.currentTarget.dataset; if (tag) { this.props.onFilterChange({ tags: [tag] }); @@ -80,61 +65,52 @@ export default class SimilarRulesFilter extends React.PureComponent<Props> { const allTags = [...tags, ...sysTags]; return ( - <Dropdown> - {({ closeDropdown, onToggleClick, open }) => { - this.closeDropdown = closeDropdown; - return ( - <div className={classNames('dropdown display-inline-block', { open })}> - <a - className="js-rule-filter link-no-underline spacer-left dropdown-toggle" - href="#" - onClick={onToggleClick}> - <i className="icon-filter icon-half-transparent" /> - <i className="icon-dropdown little-spacer-left" /> - </a> - <div className="dropdown-menu dropdown-menu-right"> - <header className="dropdown-header"> - {translate('coding_rules.filter_similar_rules')} - </header> - <ul className="menu"> - <li> - <a data-field="language" href="#" onClick={this.handleLanguageClick}> - {rule.langName} - </a> - </li> + <Dropdown + className="display-inline-block" + overlay={ + <> + <ul className="menu"> + <li className="menu-header">{translate('coding_rules.filter_similar_rules')}</li> + <li> + <a data-field="language" href="#" onClick={this.handleLanguageClick}> + {rule.langName} + </a> + </li> + + <li> + <a data-field="type" href="#" onClick={this.handleTypeClick}> + {translate('issue.type', rule.type)} + </a> + </li> - <li> - <a data-field="type" href="#" onClick={this.handleTypeClick}> - {translate('issue.type', rule.type)} - </a> - </li> + {severity && ( + <li> + <a data-field="severity" href="#" onClick={this.handleSeverityClick}> + <SeverityHelper severity={rule.severity} /> + </a> + </li> + )} - {severity && ( - <li> - <a data-field="severity" href="#" onClick={this.handleSeverityClick}> - <SeverityHelper severity={rule.severity} /> + {allTags.length > 0 && ( + <> + <li className="divider" /> + {allTags.map(tag => ( + <li key={tag}> + <a data-field="tag" data-tag={tag} href="#" onClick={this.handleTagClick}> + <i className="icon-tags icon-half-transparent little-spacer-right" /> + {tag} </a> </li> - )} - - {allTags.length > 0 && ( - <> - <li className="divider" /> - {allTags.map(tag => ( - <li key={tag}> - <a data-field="tag" data-tag={tag} href="#" onClick={this.handleTagClick}> - <i className="icon-tags icon-half-transparent little-spacer-right" /> - {tag} - </a> - </li> - ))} - </> - )} - </ul> - </div> - </div> - ); - }} + ))} + </> + )} + </ul> + </> + }> + <a className="js-rule-filter link-no-underline spacer-left dropdown-toggle" href="#"> + <i className="icon-filter icon-half-transparent" /> + <i className="icon-dropdown little-spacer-left" /> + </a> </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx deleted file mode 100644 index f4f2ff15d24..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { CustomMeasure } from '../../../app/types'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -interface Props { - measure: CustomMeasure; - onDelete: (measureId: string) => Promise<void>; -} - -export default function DeleteButton({ measure, onDelete }: Props) { - return ( - <ConfirmButton - confirmButtonText={translate('delete')} - confirmData={measure.id} - isDestructive={true} - modalBody={translateWithParameters( - 'custom_measures.delete_custom_measure.confirmation', - measure.metric.name - )} - modalHeader={translate('custom_measures.delete_custom_measure')} - onConfirm={onDelete}> - {({ onClick }) => ( - <ActionsDropdownItem - className="js-custom-measure-delete" - destructive={true} - onClick={onClick}> - {translate('delete')} - </ActionsDropdownItem> - )} - </ConfirmButton> - ); -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx new file mode 100644 index 00000000000..f8da5faad95 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { CustomMeasure } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + measure: CustomMeasure; + onClose: () => void; + onSubmit: () => Promise<void>; +} + +export default function DeleteForm({ measure, onClose, onSubmit }: Props) { + const header = translate('custom_measures.delete_custom_measure'); + + return ( + <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + {translateWithParameters( + 'custom_measures.delete_custom_measure.confirmation', + measure.metric.name + )} + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton className="button-red" disabled={submitting}> + {translate('delete')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx deleted file mode 100644 index 655b02b6eaf..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form from './Form'; -import { CustomMeasure } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; - -interface Props { - measure: CustomMeasure; - onEdit: (data: { description: string; id: string; value: string }) => Promise<void>; -} - -interface State { - modal: boolean; -} - -export default class EditButton extends React.PureComponent<Props, State> { - mounted = false; - state: State = { modal: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleClick = () => { - this.setState({ modal: true }); - }; - - handleClose = () => { - if (this.mounted) { - this.setState({ modal: false }); - } - }; - - handleSubmit = (data: { description: string; value: string }) => { - return this.props.onEdit({ id: this.props.measure.id, ...data }); - }; - - render() { - return ( - <> - <ActionsDropdownItem className="js-custom-measure-update" onClick={this.handleClick}> - {translate('update_verb')} - </ActionsDropdownItem> - {this.state.modal && ( - <Form - confirmButtonText={translate('update_verb')} - header={translate('custom_measures.update_custom_measure')} - measure={this.props.measure} - onClose={this.handleClose} - onSubmit={this.handleSubmit} - /> - )} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx new file mode 100644 index 00000000000..335d89faacd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx @@ -0,0 +1,158 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeleteForm from './DeleteForm'; +import Form from './Form'; +import MeasureDate from './MeasureDate'; +import { CustomMeasure } from '../../../app/types'; +import ActionsDropdown, { + ActionsDropdownDivider, + ActionsDropdownItem +} from '../../../components/controls/ActionsDropdown'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; + +interface Props { + measure: CustomMeasure; + onDelete: (measureId: string) => Promise<void>; + onEdit: (data: { description: string; id: string; value: string }) => Promise<void>; +} + +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class Item extends React.PureComponent<Props, State> { + mounted = false; + state: State = { + deleteForm: false, + editForm: false + }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleEditClick = () => { + this.setState({ editForm: true }); + }; + + handleDeleteClick = () => { + this.setState({ deleteForm: true }); + }; + + closeEditForm = () => { + if (this.mounted) { + this.setState({ editForm: false }); + } + }; + + closeDeleteForm = () => { + if (this.mounted) { + this.setState({ deleteForm: false }); + } + }; + + handleEditFormSubmit = (data: { description: string; value: string }) => { + return this.props.onEdit({ id: this.props.measure.id, ...data }); + }; + + handleDeleteFormSubmit = () => { + return this.props.onDelete(this.props.measure.id); + }; + + render() { + const { measure } = this.props; + + return ( + <tr data-metric={measure.metric.key}> + <td className="nowrap"> + <div> + <span className="js-custom-measure-metric-name">{measure.metric.name}</span> + {measure.pending && ( + <Tooltip overlay={translate('custom_measures.pending_tooltip')}> + <span className="js-custom-measure-pending badge badge-warning spacer-left"> + {translate('custom_measures.pending')} + </span> + </Tooltip> + )} + </div> + <span className="js-custom-measure-domain note">{measure.metric.domain}</span> + </td> + + <td className="nowrap"> + <strong className="js-custom-measure-value"> + {formatMeasure(measure.value, measure.metric.type)} + </strong> + </td> + + <td> + <span className="js-custom-measure-description">{measure.description}</span> + </td> + + <td> + <MeasureDate measure={measure} /> {translate('by_')}{' '} + <span className="js-custom-measure-user">{measure.user.name}</span> + </td> + + <td className="thin nowrap"> + <ActionsDropdown> + <ActionsDropdownItem + className="js-custom-measure-update" + onClick={this.handleEditClick}> + {translate('update_verb')} + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + className="js-custom-measure-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> + </ActionsDropdown> + </td> + + {this.state.editForm && ( + <Form + confirmButtonText={translate('update_verb')} + header={translate('custom_measures.update_custom_measure')} + measure={this.props.measure} + onClose={this.closeEditForm} + onSubmit={this.handleEditFormSubmit} + /> + )} + + {this.state.deleteForm && ( + <DeleteForm + measure={this.props.measure} + onClose={this.closeDeleteForm} + onSubmit={this.handleDeleteFormSubmit} + /> + )} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx index 239b4eeec6c..f26474192d1 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx @@ -19,16 +19,9 @@ */ import * as React from 'react'; import { sortBy } from 'lodash'; -import DeleteButton from './DeleteButton'; -import EditButton from './EditButton'; +import Item from './Item'; import { CustomMeasure } from '../../../app/types'; -import ActionsDropdown, { - ActionsDropdownDivider -} from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; -import Tooltip from '../../../components/controls/Tooltip'; -import { formatMeasure } from '../../../helpers/measures'; -import DateFormatter from '../../../components/intl/DateFormatter'; interface Props { measures: CustomMeasure[]; @@ -52,44 +45,7 @@ export default function List({ measures, onDelete, onEdit }: Props) { </thead> <tbody> {sortBy(measures, measure => measure.metric.name.toLowerCase()).map(measure => ( - <tr data-metric={measure.metric.key} key={measure.id}> - <td className="nowrap"> - <div> - <span className="js-custom-measure-metric-name">{measure.metric.name}</span> - {measure.pending && ( - <Tooltip overlay={translate('custom_measures.pending_tooltip')}> - <span className="js-custom-measure-pending badge badge-warning spacer-left"> - {translate('custom_measures.pending')} - </span> - </Tooltip> - )} - </div> - <span className="js-custom-measure-domain note">{measure.metric.domain}</span> - </td> - - <td className="nowrap"> - <strong className="js-custom-measure-value"> - {formatMeasure(measure.value, measure.metric.type)} - </strong> - </td> - - <td> - <span className="js-custom-measure-description">{measure.description}</span> - </td> - - <td> - <MeasureDate measure={measure} /> {translate('by_')}{' '} - <span className="js-custom-measure-user">{measure.user.name}</span> - </td> - - <td className="thin nowrap"> - <ActionsDropdown> - <EditButton measure={measure} onEdit={onEdit} /> - <ActionsDropdownDivider /> - <DeleteButton measure={measure} onDelete={onDelete} /> - </ActionsDropdown> - </td> - </tr> + <Item key={measure.id} measure={measure} onDelete={onDelete} onEdit={onEdit} /> ))} </tbody> </table> @@ -99,27 +55,3 @@ export default function List({ measures, onDelete, onEdit }: Props) { </div> ); } - -function MeasureDate({ measure }: { measure: CustomMeasure }) { - if (measure.updatedAt) { - return ( - <> - {translate('updated_on')}{' '} - <span className="js-custom-measure-created-at"> - <DateFormatter date={measure.updatedAt} /> - </span> - </> - ); - } else if (measure.createdAt) { - return ( - <> - {translate('created_on')}{' '} - <span className="js-custom-measure-created-at"> - <DateFormatter date={measure.createdAt} /> - </span> - </> - ); - } else { - return <>{translate('created')}</>; - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx new file mode 100644 index 00000000000..cde3f66e28d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { CustomMeasure } from '../../../app/types'; +import DateFormatter from '../../../components/intl/DateFormatter'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + measure: CustomMeasure; +} + +export default function MeasureDate({ measure }: Props) { + if (measure.updatedAt) { + return ( + <> + {translate('updated_on')}{' '} + <span className="js-custom-measure-created-at"> + <DateFormatter date={measure.updatedAt} /> + </span> + </> + ); + } else if (measure.createdAt) { + return ( + <> + {translate('created_on')}{' '} + <span className="js-custom-measure-created-at"> + <DateFormatter date={measure.createdAt} /> + </span> + </> + ); + } else { + return <>{translate('created')}</>; + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx index 3c032dafc62..411e81c2d7e 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx @@ -19,9 +19,9 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import DeleteButton from '../DeleteButton'; +import DeleteForm from '../DeleteForm'; -it('should delete custom measure', () => { +it('should render', () => { const measure = { createdAt: '2017-01-01', description: 'my custom measure', @@ -31,10 +31,7 @@ it('should delete custom measure', () => { user: { active: true, login: 'user', name: 'user' }, value: 'custom-value' }; - const onDelete = jest.fn(); - const wrapper = shallow(<DeleteButton measure={measure} onDelete={onDelete} />); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('ConfirmButton').prop<Function>('onConfirm')('1'); - expect(onDelete).toBeCalledWith('1'); + expect( + shallow(<DeleteForm measure={measure} onClose={jest.fn()} onSubmit={jest.fn()} />).dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx index f1efbc3a527..16942dfd967 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx @@ -19,27 +19,31 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import EditButton from '../EditButton'; +import Item from '../Item'; import { click } from '../../../../helpers/testUtils'; +const measure = { + createdAt: '2017-01-01', + description: 'my custom measure', + id: '1', + metric: { key: 'custom', name: 'custom-metric', type: 'STRING' }, + projectKey: 'foo', + user: { active: true, login: 'user', name: 'user' }, + value: 'custom-value' +}; + +it('should render', () => { + expect( + shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} />) + ).toMatchSnapshot(); +}); + it('should edit metric', () => { - const measure = { - createdAt: '2017-01-01', - description: 'my custom measure', - id: '1', - metric: { key: 'custom', name: 'custom-metric', type: 'STRING' }, - projectKey: 'foo', - user: { active: true, login: 'user', name: 'user' }, - value: 'custom-value' - }; const onEdit = jest.fn(); - - const wrapper = shallow(<EditButton measure={measure} onEdit={onEdit} />); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={onEdit} />); click(wrapper.find('.js-custom-measure-update')); wrapper.update(); - expect(wrapper).toMatchSnapshot(); wrapper.find('Form').prop<Function>('onSubmit')({ ...measure, @@ -48,3 +52,14 @@ it('should edit metric', () => { }); expect(onEdit).toBeCalledWith({ ...measure, description: 'new-description', value: 'new-value' }); }); + +it('should delete custom measure', () => { + const onDelete = jest.fn(); + const wrapper = shallow(<Item measure={measure} onDelete={onDelete} onEdit={jest.fn()} />); + + click(wrapper.find('.js-custom-measure-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop<Function>('onSubmit')(); + expect(onDelete).toBeCalledWith('1'); +}); diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap deleted file mode 100644 index c9fc5439615..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should delete custom measure 1`] = ` -<ConfirmButton - confirmButtonText="delete" - confirmData="1" - isDestructive={true} - modalBody="custom_measures.delete_custom_measure.confirmation.custom-metric" - modalHeader="custom_measures.delete_custom_measure" - onConfirm={[MockFunction]} -/> -`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..77d8286a180 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<Modal + contentLabel="custom_measures.delete_custom_measure" + onRequestClose={[MockFunction]} +> + <form + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + custom_measures.delete_custom_measure + </h2> + </header> + <div + className="modal-body" + > + custom_measures.delete_custom_measure.confirmation.custom-metric + </div> + <footer + className="modal-foot" + > + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} + /> + <SubmitButton + className="button-red" + disabled={false} + > + delete + </SubmitButton> + <ResetButtonLink + disabled={false} + onClick={[Function]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap deleted file mode 100644 index bd0a8035083..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit metric 1`] = ` -<React.Fragment> - <ActionsDropdownItem - className="js-custom-measure-update" - onClick={[Function]} - > - update_verb - </ActionsDropdownItem> -</React.Fragment> -`; - -exports[`should edit metric 2`] = ` -<React.Fragment> - <ActionsDropdownItem - className="js-custom-measure-update" - onClick={[Function]} - > - update_verb - </ActionsDropdownItem> - <Form - confirmButtonText="update_verb" - header="custom_measures.update_custom_measure" - measure={ - Object { - "createdAt": "2017-01-01", - "description": "my custom measure", - "id": "1", - "metric": Object { - "key": "custom", - "name": "custom-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "custom-value", - } - } - onClose={[Function]} - onSubmit={[Function]} - /> -</React.Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap new file mode 100644 index 00000000000..225ca0c587c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<tr + data-metric="custom" +> + <td + className="nowrap" + > + <div> + <span + className="js-custom-measure-metric-name" + > + custom-metric + </span> + </div> + <span + className="js-custom-measure-domain note" + /> + </td> + <td + className="nowrap" + > + <strong + className="js-custom-measure-value" + > + custom-value + </strong> + </td> + <td> + <span + className="js-custom-measure-description" + > + my custom measure + </span> + </td> + <td> + <MeasureDate + measure={ + Object { + "createdAt": "2017-01-01", + "description": "my custom measure", + "id": "1", + "metric": Object { + "key": "custom", + "name": "custom-metric", + "type": "STRING", + }, + "projectKey": "foo", + "user": Object { + "active": true, + "login": "user", + "name": "user", + }, + "value": "custom-value", + } + } + /> + + by_ + + <span + className="js-custom-measure-user" + > + user + </span> + </td> + <td + className="thin nowrap" + > + <ActionsDropdown> + <ActionsDropdownItem + className="js-custom-measure-update" + onClick={[Function]} + > + update_verb + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + className="js-custom-measure-delete" + destructive={true} + onClick={[Function]} + > + delete + </ActionsDropdownItem> + </ActionsDropdown> + </td> +</tr> +`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap index 005bedff1e0..14d2a0c362d 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap @@ -26,235 +26,53 @@ exports[`should render 1`] = ` </tr> </thead> <tbody> - <tr - data-metric="another" + <Item key="2" - > - <td - className="nowrap" - > - <div> - <span - className="js-custom-measure-metric-name" - > - another-metric - </span> - </div> - <span - className="js-custom-measure-domain note" - /> - </td> - <td - className="nowrap" - > - <strong - className="js-custom-measure-value" - > - another-value - </strong> - </td> - <td> - <span - className="js-custom-measure-description" - /> - </td> - <td> - <MeasureDate - measure={ - Object { - "createdAt": "2017-01-01", - "id": "2", - "metric": Object { - "key": "another", - "name": "another-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "another-value", - } - } - /> - - by_ - - <span - className="js-custom-measure-user" - > - user - </span> - </td> - <td - className="thin nowrap" - > - <ActionsDropdown> - <EditButton - measure={ - Object { - "createdAt": "2017-01-01", - "id": "2", - "metric": Object { - "key": "another", - "name": "another-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "another-value", - } - } - onEdit={[MockFunction]} - /> - <ActionsDropdownDivider /> - <DeleteButton - measure={ - Object { - "createdAt": "2017-01-01", - "id": "2", - "metric": Object { - "key": "another", - "name": "another-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "another-value", - } - } - onDelete={[MockFunction]} - /> - </ActionsDropdown> - </td> - </tr> - <tr - data-metric="custom" + measure={ + Object { + "createdAt": "2017-01-01", + "id": "2", + "metric": Object { + "key": "another", + "name": "another-metric", + "type": "STRING", + }, + "projectKey": "foo", + "user": Object { + "active": true, + "login": "user", + "name": "user", + }, + "value": "another-value", + } + } + onDelete={[MockFunction]} + onEdit={[MockFunction]} + /> + <Item key="1" - > - <td - className="nowrap" - > - <div> - <span - className="js-custom-measure-metric-name" - > - custom-metric - </span> - </div> - <span - className="js-custom-measure-domain note" - /> - </td> - <td - className="nowrap" - > - <strong - className="js-custom-measure-value" - > - custom-value - </strong> - </td> - <td> - <span - className="js-custom-measure-description" - > - my custom measure - </span> - </td> - <td> - <MeasureDate - measure={ - Object { - "createdAt": "2017-01-01", - "description": "my custom measure", - "id": "1", - "metric": Object { - "key": "custom", - "name": "custom-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "custom-value", - } - } - /> - - by_ - - <span - className="js-custom-measure-user" - > - user - </span> - </td> - <td - className="thin nowrap" - > - <ActionsDropdown> - <EditButton - measure={ - Object { - "createdAt": "2017-01-01", - "description": "my custom measure", - "id": "1", - "metric": Object { - "key": "custom", - "name": "custom-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "custom-value", - } - } - onEdit={[MockFunction]} - /> - <ActionsDropdownDivider /> - <DeleteButton - measure={ - Object { - "createdAt": "2017-01-01", - "description": "my custom measure", - "id": "1", - "metric": Object { - "key": "custom", - "name": "custom-metric", - "type": "STRING", - }, - "projectKey": "foo", - "user": Object { - "active": true, - "login": "user", - "name": "user", - }, - "value": "custom-value", - } - } - onDelete={[MockFunction]} - /> - </ActionsDropdown> - </td> - </tr> + measure={ + Object { + "createdAt": "2017-01-01", + "description": "my custom measure", + "id": "1", + "metric": Object { + "key": "custom", + "name": "custom-metric", + "type": "STRING", + }, + "projectKey": "foo", + "user": Object { + "active": true, + "login": "user", + "name": "user", + }, + "value": "custom-value", + } + } + onDelete={[MockFunction]} + onEdit={[MockFunction]} + /> </tbody> </table> </div> diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx deleted file mode 100644 index d825823c7f3..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { Metric } from '../../../app/types'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -interface Props { - metric: Metric; - onDelete: (metricKey: string) => Promise<void>; -} - -export default function DeleteButton({ metric, onDelete }: Props) { - return ( - <ConfirmButton - confirmButtonText={translate('delete')} - confirmData={metric.key} - isDestructive={true} - modalBody={translateWithParameters('custom_metrics.delete_metric.confirmation', metric.name)} - modalHeader={translate('custom_metrics.delete_metric')} - onConfirm={onDelete}> - {({ onClick }) => ( - <ActionsDropdownItem className="js-metric-delete" destructive={true} onClick={onClick}> - {translate('delete')} - </ActionsDropdownItem> - )} - </ConfirmButton> - ); -} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx new file mode 100644 index 00000000000..c799bcb1a56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Metric } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + metric: Metric; + onClose: () => void; + onSubmit: () => Promise<void>; +} + +export default function DeleteForm({ metric, onClose, onSubmit }: Props) { + const header = translate('custom_metrics.delete_metric'); + + return ( + <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + {translateWithParameters('custom_metrics.delete_metric.confirmation', metric.name)} + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton className="button-red" disabled={submitting}> + {translate('delete')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); +} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx deleted file mode 100644 index d0870fa8c06..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form, { MetricProps } from './Form'; -import { Metric } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; - -interface Props { - domains: string[]; - metric: Metric; - onEdit: (data: { id: string } & MetricProps) => Promise<void>; - types: string[]; -} - -interface State { - modal: boolean; -} - -export default class EditButton extends React.PureComponent<Props, State> { - mounted = false; - state: State = { modal: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleClick = () => { - this.setState({ modal: true }); - }; - - handleClose = () => { - if (this.mounted) { - this.setState({ modal: false }); - } - }; - - handleSubmit = (data: MetricProps) => { - return this.props.onEdit({ id: this.props.metric.id, ...data }); - }; - - render() { - return ( - <> - <ActionsDropdownItem className="js-metric-update" onClick={this.handleClick}> - {translate('update_details')} - </ActionsDropdownItem> - {this.state.modal && ( - <Form - confirmButtonText={translate('update_verb')} - domains={this.props.domains} - header={translate('custom_metrics.update_metric')} - metric={this.props.metric} - onClose={this.handleClose} - onSubmit={this.handleSubmit} - types={this.props.types} - /> - )} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx new file mode 100644 index 00000000000..7f31c648bf3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx @@ -0,0 +1,150 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeleteForm from './DeleteForm'; +import Form, { MetricProps } from './Form'; +import { Metric } from '../../../app/types'; +import ActionsDropdown, { + ActionsDropdownDivider, + ActionsDropdownItem +} from '../../../components/controls/ActionsDropdown'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + domains?: string[]; + metric: Metric; + onDelete: (metricKey: string) => Promise<void>; + onEdit: (data: { id: string } & MetricProps) => Promise<void>; + types?: string[]; +} + +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class Item extends React.PureComponent<Props, State> { + mounted = false; + + state: State = { deleteForm: false, editForm: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleEditClick = () => { + this.setState({ editForm: true }); + }; + + handleDeleteClick = () => { + this.setState({ deleteForm: true }); + }; + + closeEditForm = () => { + if (this.mounted) { + this.setState({ editForm: false }); + } + }; + + closeDeleteForm = () => { + if (this.mounted) { + this.setState({ deleteForm: false }); + } + }; + + handleEditFormSubmit = (data: MetricProps) => { + return this.props.onEdit({ id: this.props.metric.id, ...data }); + }; + + handleDeleteFormSubmit = () => { + return this.props.onDelete(this.props.metric.key); + }; + + render() { + const { domains, metric, types } = this.props; + + return ( + <tr data-metric={metric.key}> + <td className="width-30"> + <div> + <strong className="js-metric-name">{metric.name}</strong> + <span className="js-metric-key note little-spacer-left">{metric.key}</span> + </div> + </td> + + <td className="width-20"> + <span className="js-metric-domain">{metric.domain}</span> + </td> + + <td className="width-20"> + <span className="js-metric-type">{translate('metric.type', metric.type)}</span> + </td> + + <td className="width-20" title={metric.description}> + <span className="js-metric-description">{metric.description}</span> + </td> + + <td className="thin nowrap"> + <ActionsDropdown> + {domains && + types && ( + <ActionsDropdownItem className="js-metric-update" onClick={this.handleEditClick}> + {translate('update_details')} + </ActionsDropdownItem> + )} + <ActionsDropdownDivider /> + <ActionsDropdownItem + className="js-metric-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> + </ActionsDropdown> + </td> + + {this.state.editForm && + domains && + types && ( + <Form + confirmButtonText={translate('update_verb')} + domains={domains} + header={translate('custom_metrics.update_metric')} + metric={metric} + onClose={this.closeEditForm} + onSubmit={this.handleEditFormSubmit} + types={types} + /> + )} + + {this.state.deleteForm && ( + <DeleteForm + metric={metric} + onClose={this.closeDeleteForm} + onSubmit={this.handleDeleteFormSubmit} + /> + )} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx index a5449bf68b6..ddbf383ec2d 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx @@ -19,13 +19,9 @@ */ import * as React from 'react'; import { sortBy } from 'lodash'; -import DeleteButton from './DeleteButton'; -import EditButton from './EditButton'; import { MetricProps } from './Form'; +import Item from './Item'; import { Metric } from '../../../app/types'; -import ActionsDropdown, { - ActionsDropdownDivider -} from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -43,42 +39,14 @@ export default function List({ domains, metrics, onDelete, onEdit, types }: Prop <table className="data zebra zebra-hover"> <tbody> {sortBy(metrics, metric => metric.name.toLowerCase()).map(metric => ( - <tr data-metric={metric.key} key={metric.key}> - <td className="width-30"> - <div> - <strong className="js-metric-name">{metric.name}</strong> - <span className="js-metric-key note little-spacer-left">{metric.key}</span> - </div> - </td> - - <td className="width-20"> - <span className="js-metric-domain">{metric.domain}</span> - </td> - - <td className="width-20"> - <span className="js-metric-type">{translate('metric.type', metric.type)}</span> - </td> - - <td className="width-20" title={metric.description}> - <span className="js-metric-description">{metric.description}</span> - </td> - - <td className="thin nowrap"> - <ActionsDropdown> - {domains && - types && ( - <EditButton - domains={domains} - metric={metric} - onEdit={onEdit} - types={types} - /> - )} - <ActionsDropdownDivider /> - <DeleteButton metric={metric} onDelete={onDelete} /> - </ActionsDropdown> - </td> - </tr> + <Item + domains={domains} + key={metric.key} + metric={metric} + onDelete={onDelete} + onEdit={onEdit} + types={types} + /> ))} </tbody> </table> diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx index a35e39a610e..bbfe2d0f2b1 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx @@ -19,14 +19,11 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import DeleteButton from '../DeleteButton'; +import DeleteForm from '../DeleteForm'; -it('should delete metric', () => { +it('should render', () => { const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; - const onDelete = jest.fn(); - const wrapper = shallow(<DeleteButton metric={metric} onDelete={onDelete} />); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('ConfirmButton').prop<Function>('onConfirm')('foo'); - expect(onDelete).toBeCalledWith('foo'); + expect( + shallow(<DeleteForm metric={metric} onClose={jest.fn()} onSubmit={jest.fn()} />).dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx index 5fce34493a4..dc731037d09 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx @@ -19,26 +19,32 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import EditButton from '../EditButton'; +import Item from '../Item'; import { click } from '../../../../helpers/testUtils'; +const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; + +it('should render', () => { + expect( + shallow(<Item metric={metric} onDelete={jest.fn()} onEdit={jest.fn()} />) + ).toMatchSnapshot(); +}); + it('should edit metric', () => { - const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; const onEdit = jest.fn(); const wrapper = shallow( - <EditButton + <Item domains={['Coverage', 'Issues']} metric={metric} + onDelete={jest.fn()} onEdit={onEdit} types={['INT', 'STRING']} /> ); - expect(wrapper).toMatchSnapshot(); click(wrapper.find('.js-metric-update')); wrapper.update(); - expect(wrapper).toMatchSnapshot(); wrapper.find('Form').prop<Function>('onSubmit')({ ...metric, @@ -47,3 +53,14 @@ it('should edit metric', () => { }); expect(onEdit).toBeCalledWith({ ...metric, description: 'bla bla', domain: 'Coverage' }); }); + +it('should delete metric', () => { + const onDelete = jest.fn(); + const wrapper = shallow(<Item metric={metric} onDelete={onDelete} onEdit={jest.fn()} />); + + click(wrapper.find('.js-metric-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop<Function>('onSubmit')(); + expect(onDelete).toBeCalledWith('foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap deleted file mode 100644 index bd6e9cd420b..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should delete metric 1`] = ` -<ConfirmButton - confirmButtonText="delete" - confirmData="foo" - isDestructive={true} - modalBody="custom_metrics.delete_metric.confirmation.Foo" - modalHeader="custom_metrics.delete_metric" - onConfirm={[MockFunction]} -/> -`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..cd0903c2c8a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<Modal + contentLabel="custom_metrics.delete_metric" + onRequestClose={[MockFunction]} +> + <form + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + custom_metrics.delete_metric + </h2> + </header> + <div + className="modal-body" + > + custom_metrics.delete_metric.confirmation.Foo + </div> + <footer + className="modal-foot" + > + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} + /> + <SubmitButton + className="button-red" + disabled={false} + > + delete + </SubmitButton> + <ResetButtonLink + disabled={false} + onClick={[Function]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap deleted file mode 100644 index 66d078530d4..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit metric 1`] = ` -<React.Fragment> - <ActionsDropdownItem - className="js-metric-update" - onClick={[Function]} - > - update_details - </ActionsDropdownItem> -</React.Fragment> -`; - -exports[`should edit metric 2`] = ` -<React.Fragment> - <ActionsDropdownItem - className="js-metric-update" - onClick={[Function]} - > - update_details - </ActionsDropdownItem> - <Form - confirmButtonText="update_verb" - domains={ - Array [ - "Coverage", - "Issues", - ] - } - header="custom_metrics.update_metric" - metric={ - Object { - "id": "3", - "key": "foo", - "name": "Foo", - "type": "INT", - } - } - onClose={[Function]} - onSubmit={[Function]} - types={ - Array [ - "INT", - "STRING", - ] - } - /> -</React.Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap new file mode 100644 index 00000000000..dbef9673e8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<tr + data-metric="foo" +> + <td + className="width-30" + > + <div> + <strong + className="js-metric-name" + > + Foo + </strong> + <span + className="js-metric-key note little-spacer-left" + > + foo + </span> + </div> + </td> + <td + className="width-20" + > + <span + className="js-metric-domain" + /> + </td> + <td + className="width-20" + > + <span + className="js-metric-type" + > + metric.type.INT + </span> + </td> + <td + className="width-20" + > + <span + className="js-metric-description" + /> + </td> + <td + className="thin nowrap" + > + <ActionsDropdown> + <ActionsDropdownDivider /> + <ActionsDropdownItem + className="js-metric-delete" + destructive={true} + onClick={[Function]} + > + delete + </ActionsDropdownItem> + </ActionsDropdown> + </td> +</tr> +`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap index 1fb174588c7..31507c7f042 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap @@ -9,133 +9,33 @@ exports[`should render 1`] = ` className="data zebra zebra-hover" > <tbody> - <tr - data-metric="bar" + <Item key="bar" - > - <td - className="width-30" - > - <div> - <strong - className="js-metric-name" - > - Bar - </strong> - <span - className="js-metric-key note little-spacer-left" - > - bar - </span> - </div> - </td> - <td - className="width-20" - > - <span - className="js-metric-domain" - > - Coverage - </span> - </td> - <td - className="width-20" - > - <span - className="js-metric-type" - > - metric.type.INT - </span> - </td> - <td - className="width-20" - > - <span - className="js-metric-description" - /> - </td> - <td - className="thin nowrap" - > - <ActionsDropdown> - <ActionsDropdownDivider /> - <DeleteButton - metric={ - Object { - "domain": "Coverage", - "id": "4", - "key": "bar", - "name": "Bar", - "type": "INT", - } - } - onDelete={[MockFunction]} - /> - </ActionsDropdown> - </td> - </tr> - <tr - data-metric="foo" + metric={ + Object { + "domain": "Coverage", + "id": "4", + "key": "bar", + "name": "Bar", + "type": "INT", + } + } + onDelete={[MockFunction]} + onEdit={[MockFunction]} + /> + <Item key="foo" - > - <td - className="width-30" - > - <div> - <strong - className="js-metric-name" - > - Foo - </strong> - <span - className="js-metric-key note little-spacer-left" - > - foo - </span> - </div> - </td> - <td - className="width-20" - > - <span - className="js-metric-domain" - /> - </td> - <td - className="width-20" - > - <span - className="js-metric-type" - > - metric.type.INT - </span> - </td> - <td - className="width-20" - > - <span - className="js-metric-description" - /> - </td> - <td - className="thin nowrap" - > - <ActionsDropdown> - <ActionsDropdownDivider /> - <DeleteButton - metric={ - Object { - "id": "3", - "key": "foo", - "name": "Foo", - "type": "INT", - } - } - onDelete={[MockFunction]} - /> - </ActionsDropdown> - </td> - </tr> + metric={ + Object { + "id": "3", + "key": "foo", + "name": "Foo", + "type": "INT", + } + } + onDelete={[MockFunction]} + onEdit={[MockFunction]} + /> </tbody> </table> </div> diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx new file mode 100644 index 00000000000..a1a3a495677 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Group } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + group: Group; + onClose: () => void; + onSubmit: () => Promise<void>; +} + +export default function DeleteForm({ group, onClose, onSubmit }: Props) { + const header = translate('groups.delete_group'); + + return ( + <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + {translateWithParameters('groups.delete_group.confirmation', group.name)} + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton className="button-red" disabled={submitting}> + {translate('delete')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx deleted file mode 100644 index 2de9ded54a1..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form from './Form'; -import { Group } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { omitNil } from '../../../helpers/request'; - -interface Props { - children: (props: { onClick: () => void }) => React.ReactNode; - group: Group; - onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>; -} - -interface State { - modal: boolean; -} - -export default class EditGroup extends React.PureComponent<Props, State> { - mounted = false; - state: State = { modal: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleClick = () => { - this.setState({ modal: true }); - }; - - handleClose = () => { - if (this.mounted) { - this.setState({ modal: false }); - } - }; - - handleSubmit = ({ name, description }: { name: string; description: string }) => { - const { group } = this.props; - return this.props.onEdit({ - description, - id: group.id, - // pass `name` only if it has changed, otherwise the WS fails - ...omitNil({ name: name !== group.name ? name : undefined }) - }); - }; - - render() { - return ( - <> - {this.props.children({ onClick: this.handleClick })} - {this.state.modal && ( - <Form - confirmButtonText={translate('update_verb')} - group={this.props.group} - header={translate('groups.update_group')} - onClose={this.handleClose} - onSubmit={this.handleSubmit} - /> - )} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx index 651066f64b0..a1f2f8ba611 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx @@ -18,15 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import EditGroup from './EditGroup'; +import DeleteForm from './DeleteForm'; import EditMembers from './EditMembers'; +import Form from './Form'; import { Group } from '../../../app/types'; import ActionsDropdown, { ActionsDropdownItem, ActionsDropdownDivider } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; +import { omitNil } from '../../../helpers/request'; interface Props { group: Group; @@ -36,11 +37,57 @@ interface Props { organization: string | undefined; } -export default class ListItem extends React.PureComponent<Props> { - handleDelete = () => { +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class ListItem extends React.PureComponent<Props, State> { + mounted = false; + state: State = { deleteForm: false, editForm: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleDeleteClick = () => { + this.setState({ deleteForm: true }); + }; + + handleEditClick = () => { + this.setState({ editForm: true }); + }; + + closeDeleteForm = () => { + if (this.mounted) { + this.setState({ deleteForm: false }); + } + }; + + closeEditForm = () => { + if (this.mounted) { + this.setState({ editForm: false }); + } + }; + + handleDeleteFormSubmit = () => { return this.props.onDelete(this.props.group.name); }; + handleEditFormSubmit = ({ name, description }: { name: string; description: string }) => { + const { group } = this.props; + return this.props.onEdit({ + description, + id: group.id, + // pass `name` only if it has changed, otherwise the WS fails + ...omitNil({ name: name !== group.name ? name : undefined }) + }); + }; + render() { const { group } = this.props; @@ -71,32 +118,37 @@ export default class ListItem extends React.PureComponent<Props> { <td className="thin nowrap text-right"> {!group.default && ( <ActionsDropdown> - <EditGroup group={group} onEdit={this.props.onEdit}> - {({ onClick }) => ( - <ActionsDropdownItem className="js-group-update" onClick={onClick}> - {translate('update_details')} - </ActionsDropdownItem> - )} - </EditGroup> + <ActionsDropdownItem className="js-group-update" onClick={this.handleEditClick}> + {translate('update_details')} + </ActionsDropdownItem> <ActionsDropdownDivider /> - <ConfirmButton - confirmButtonText={translate('delete')} - isDestructive={true} - modalBody={translateWithParameters('groups.delete_group.confirmation', group.name)} - modalHeader={translate('groups.delete_group')} - onConfirm={this.handleDelete}> - {({ onClick }) => ( - <ActionsDropdownItem - className="js-group-delete" - destructive={true} - onClick={onClick}> - {translate('delete')} - </ActionsDropdownItem> - )} - </ConfirmButton> + <ActionsDropdownItem + className="js-group-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> </ActionsDropdown> )} </td> + + {this.state.deleteForm && ( + <DeleteForm + group={group} + onClose={this.closeDeleteForm} + onSubmit={this.handleDeleteFormSubmit} + /> + )} + + {this.state.editForm && ( + <Form + confirmButtonText={translate('update_verb')} + group={group} + header={translate('groups.update_group')} + onClose={this.closeEditForm} + onSubmit={this.handleEditFormSubmit} + /> + )} </tr> ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx index 750dd8a89a3..9aa65bc38a9 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx @@ -17,20 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; -import BubblePopup from '../BubblePopup'; +import DeleteForm from '../DeleteForm'; -const props = { - position: { top: 0, right: 0 }, - customClass: 'custom' -}; - -it('should render popup', () => { - const popup = shallow( - <BubblePopup {...props}> - <span>test</span> - </BubblePopup> - ); - expect(popup).toMatchSnapshot(); +it('should render', () => { + const group = { id: 3, name: 'Foo', membersCount: 5 }; + expect( + shallow(<DeleteForm group={group} onClose={jest.fn()} onSubmit={jest.fn()} />).dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx deleted file mode 100644 index d650c321233..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { shallow } from 'enzyme'; -import EditGroup from '../EditGroup'; - -it('should edit group', () => { - const group = { id: 3, name: 'Foo', membersCount: 5 }; - const onEdit = jest.fn(); - const newDescription = 'bla bla'; - let onClick: any; - - const wrapper = shallow( - <EditGroup group={group} onEdit={onEdit}> - {props => { - ({ onClick } = props); - return <div />; - }} - </EditGroup> - ); - expect(wrapper).toMatchSnapshot(); - - onClick(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); - - // change name - wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: newDescription }); - expect(onEdit).lastCalledWith({ description: newDescription, id: 3, name: 'Bar' }); - - // change description - wrapper.find('Form').prop<Function>('onSubmit')({ - name: group.name, - description: newDescription - }); - expect(onEdit).lastCalledWith({ description: newDescription, id: group.id }); - - wrapper.find('Form').prop<Function>('onClose')(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx index 28c9bb1617a..805c151b5cb 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx @@ -20,6 +20,27 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ListItem from '../ListItem'; +import { click } from '../../../../helpers/testUtils'; + +it('should edit group', () => { + const group = { id: 3, name: 'Foo', membersCount: 5 }; + const onEdit = jest.fn(); + const wrapper = shallow( + <ListItem + group={group} + onDelete={jest.fn()} + onEdit={onEdit} + onEditMembers={jest.fn()} + organization="org" + /> + ); + + click(wrapper.find('.js-group-update')); + wrapper.update(); + + wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: 'bla bla' }); + expect(onEdit).lastCalledWith({ description: 'bla bla', id: 3, name: 'Bar' }); +}); it('should delete group', () => { const group = { id: 3, name: 'Foo', membersCount: 5 }; @@ -35,7 +56,10 @@ it('should delete group', () => { ); expect(wrapper).toMatchSnapshot(); - wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); + click(wrapper.find('.js-group-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop<Function>('onSubmit')(); expect(onDelete).toBeCalledWith('Foo'); }); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..4d93bc7cf40 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<Modal + contentLabel="groups.delete_group" + onRequestClose={[MockFunction]} +> + <form + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + groups.delete_group + </h2> + </header> + <div + className="modal-body" + > + groups.delete_group.confirmation.Foo + </div> + <footer + className="modal-foot" + > + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} + /> + <SubmitButton + className="button-red" + disabled={false} + > + delete + </SubmitButton> + <ResetButtonLink + disabled={false} + onClick={[Function]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap deleted file mode 100644 index 43bc8b832cb..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit group 1`] = ` -<React.Fragment> - <div /> -</React.Fragment> -`; - -exports[`should edit group 2`] = ` -<React.Fragment> - <div /> - <Form - confirmButtonText="update_verb" - group={ - Object { - "id": 3, - "membersCount": 5, - "name": "Foo", - } - } - header="groups.update_group" - onClose={[Function]} - onSubmit={[Function]} - /> -</React.Fragment> -`; - -exports[`should edit group 3`] = ` -<React.Fragment> - <div /> -</React.Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap index 2146de899da..59d94b8e22b 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap @@ -48,24 +48,20 @@ exports[`should delete group 1`] = ` className="thin nowrap text-right" > <ActionsDropdown> - <EditGroup - group={ - Object { - "id": 3, - "membersCount": 5, - "name": "Foo", - } - } - onEdit={[MockFunction]} - /> + <ActionsDropdownItem + className="js-group-update" + onClick={[Function]} + > + update_details + </ActionsDropdownItem> <ActionsDropdownDivider /> - <ConfirmButton - confirmButtonText="delete" - isDestructive={true} - modalBody="groups.delete_group.confirmation.Foo" - modalHeader="groups.delete_group" - onConfirm={[Function]} - /> + <ActionsDropdownItem + className="js-group-delete" + destructive={true} + onClick={[Function]} + > + delete + </ActionsDropdownItem> </ActionsDropdown> </td> </tr> diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index df3a3a1374c..95d0e0dacea 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import Helmet from 'react-helmet'; -import * as classNames from 'classnames'; import * as key from 'keymaster'; import { keyBy, union, without } from 'lodash'; import * as PropTypes from 'prop-types'; @@ -804,27 +803,26 @@ export default class App extends React.PureComponent<Props, State> { thirdState={thirdState} /> {checked.length > 0 ? ( - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('dropdown display-inline-block', { open })}> - <Button id="issues-bulk-change" onClick={onToggleClick}> - {translate('bulk_change')} - <i className="icon-dropdown little-spacer-left" /> - </Button> - <ul className="dropdown-menu"> - <li> - <a href="#" onClick={this.handleBulkChangeClick}> - {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} - </a> - </li> - <li> - <a href="#" onClick={this.handleBulkChangeSelectedClick}> - {translateWithParameters('issues.bulk_change_selected', checked.length)} - </a> - </li> - </ul> - </div> - )} + <Dropdown + className="display-inline-block" + overlay={ + <ul className="menu"> + <li> + <a href="#" onClick={this.handleBulkChangeClick}> + {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} + </a> + </li> + <li> + <a href="#" onClick={this.handleBulkChangeSelectedClick}> + {translateWithParameters('issues.bulk_change_selected', checked.length)} + </a> + </li> + </ul> + }> + <Button id="issues-bulk-change"> + {translate('bulk_change')} + <i className="icon-dropdown little-spacer-left" /> + </Button> </Dropdown> ) : ( <Button id="issues-bulk-change" onClick={this.handleBulkChangeClick}> diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx index 26b333cc334..080399bbe71 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx @@ -19,36 +19,32 @@ */ import * as React from 'react'; import PluginChangeLogItem from './PluginChangeLogItem'; -import BubblePopup from '../../../components/common/BubblePopup'; import { Release, Update } from '../../../api/plugins'; import { translate } from '../../../helpers/l10n'; interface Props { - popupPosition?: any; release: Release; update: Update; } -export default function PluginChangeLog({ popupPosition, release, update }: Props) { +export default function PluginChangeLog({ release, update }: Props) { return ( - <BubblePopup position={popupPosition} customClass="bubble-popup-bottom-right"> - <div className="abs-width-300 bubble-popup-container"> - <div className="bubble-popup-title">{translate('changelog')}</div> - <ul className="js-plugin-changelog-list"> - {update.previousUpdates && - update.previousUpdates.map( - previousUpdate => - previousUpdate.release ? ( - <PluginChangeLogItem - key={previousUpdate.release.version} - release={previousUpdate.release} - update={previousUpdate} - /> - ) : null - )} - <PluginChangeLogItem release={release} update={update} /> - </ul> - </div> - </BubblePopup> + <div className="abs-width-300"> + <h6>{translate('changelog')}</h6> + <ul className="js-plugin-changelog-list"> + {update.previousUpdates && + update.previousUpdates.map( + previousUpdate => + previousUpdate.release ? ( + <PluginChangeLogItem + key={previousUpdate.release.version} + release={previousUpdate.release} + update={previousUpdate} + /> + ) : null + )} + <PluginChangeLogItem release={release} update={update} /> + </ul> + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx index b542908b5c2..a2ff350334b 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import PluginChangeLog from './PluginChangeLog'; import { Release, Update } from '../../../api/plugins'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Dropdown from '../../../components/controls/Dropdown'; import { Button } from '../../../components/ui/buttons'; interface Props { @@ -28,39 +28,12 @@ interface Props { update: Update; } -interface State { - changelogOpen: boolean; -} - -export default class PluginChangeLogButton extends React.PureComponent<Props, State> { - state: State = { changelogOpen: false }; - - toggleChangelog = (show?: boolean) => { - if (show !== undefined) { - this.setState({ changelogOpen: show }); - } else { - this.setState(state => ({ changelogOpen: !state.changelogOpen })); - } - }; - - handleClick = () => { - this.toggleChangelog(); - }; - - render() { - return ( - <div className="display-inline-block little-spacer-left"> - <Button - className="button-link js-changelog issue-rule icon-ellipsis-h" - onClick={this.handleClick} - /> - <BubblePopupHelper - isOpen={this.state.changelogOpen} - popup={<PluginChangeLog release={this.props.release} update={this.props.update} />} - position="bottomright" - togglePopup={this.toggleChangelog} - /> - </div> - ); - } +export default function PluginChangeLogButton({ release, update }: Props) { + return ( + <Dropdown + className="display-inline-block little-spacer-left" + overlay={<PluginChangeLog release={release} update={update} />}> + <Button className="button-link js-changelog issue-rule icon-ellipsis-h" /> + </Dropdown> + ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js index 891502d42e6..792f218ffac 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js @@ -22,10 +22,11 @@ import React from 'react'; import RemoveMemberForm from './forms/RemoveMemberForm'; import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm'; import Avatar from '../../../components/ui/Avatar'; -import { translateWithParameters } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import ActionsDropdown, { - ActionsDropdownDivider + ActionsDropdownDivider, + ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; /*:: import type { Member } from '../../../store/organizationsMembers/actions'; */ /*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */ @@ -38,12 +39,47 @@ type Props = { removeMember: Member => void, updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void }; + +type State = { + removeMemberForm: bool, + manageGroupsForm: bool +} */ const AVATAR_SIZE /*: number */ = 36; export default class MembersListItem extends React.PureComponent { + mounted /*: bool */ = false; /*:: props: Props; */ + state /*: State */ = { removeMemberForm: false, manageGroupsForm: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleManageGroupsClick = () => { + this.setState({ manageGroupsForm: true }); + }; + + closeManageGroupsForm = () => { + if (this.mounted) { + this.setState({ manageGroupsForm: false }); + } + }; + + handleRemoveMemberClick = () => { + this.setState({ removeMemberForm: true }); + }; + + closeRemoveMemberForm = () => { + if (this.mounted) { + this.setState({ removeMemberForm: false }); + } + }; render() { const { member, organization } = this.props; @@ -65,22 +101,38 @@ export default class MembersListItem extends React.PureComponent { </td> )} {organization.canAdmin && ( - <td className="nowrap text-middle text-right"> - <ActionsDropdown> + <React.Fragment> + <td className="nowrap text-middle text-right"> + <ActionsDropdown> + <ActionsDropdownItem onClick={this.handleManageGroupsClick}> + {translate('organization.members.manage_groups')} + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem destructive={true} onClick={this.handleRemoveMemberClick}> + {translate('organization.members.remove')} + </ActionsDropdownItem> + </ActionsDropdown> + </td> + + {this.state.manageGroupsForm && ( <ManageMemberGroupsForm - organizationGroups={this.props.organizationGroups} + member={this.props.member} + onClose={this.closeManageGroupsForm} organization={this.props.organization} + organizationGroups={this.props.organizationGroups} updateMemberGroups={this.props.updateMemberGroups} - member={this.props.member} /> - <ActionsDropdownDivider /> + )} + + {this.state.removeMemberForm && ( <RemoveMemberForm + member={this.props.member} + onClose={this.closeRemoveMemberForm} organization={this.props.organization} removeMember={this.props.removeMember} - member={this.props.member} /> - </ActionsDropdown> - </td> + )} + </React.Fragment> )} </tr> ); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap index b88cbc6b739..2f04b608477 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap @@ -28,45 +28,26 @@ exports[`should groups at 0 if the groupCount field is not defined (just added u > organization.members.x_groups.0 </td> - <td - className="nowrap text-middle text-right" - > - <ActionsDropdown> - <ManageMemberGroupsForm - member={ - Object { - "avatar": "7daf6c79d4802916d83f6266e24850af", - "login": "john", - "name": "John Doe", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - <ActionsDropdownDivider /> - <RemoveMemberForm - member={ - Object { - "avatar": "7daf6c79d4802916d83f6266e24850af", - "login": "john", - "name": "John Doe", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </ActionsDropdown> - </td> + <React.Fragment> + <td + className="nowrap text-middle text-right" + > + <ActionsDropdown> + <ActionsDropdownItem + onClick={[Function]} + > + organization.members.manage_groups + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + destructive={true} + onClick={[Function]} + > + organization.members.remove + </ActionsDropdownItem> + </ActionsDropdown> + </td> + </React.Fragment> </tr> `; @@ -124,46 +105,25 @@ exports[`should render actions and groups for admin 1`] = ` > organization.members.x_groups.3 </td> - <td - className="nowrap text-middle text-right" - > - <ActionsDropdown> - <ManageMemberGroupsForm - member={ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - <ActionsDropdownDivider /> - <RemoveMemberForm - member={ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </ActionsDropdown> - </td> + <React.Fragment> + <td + className="nowrap text-middle text-right" + > + <ActionsDropdown> + <ActionsDropdownItem + onClick={[Function]} + > + organization.members.manage_groups + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + destructive={true} + onClick={[Function]} + > + organization.members.remove + </ActionsDropdownItem> + </ActionsDropdown> + </td> + </React.Fragment> </tr> `; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js index 3c748a3588c..4339538beb1 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js @@ -24,12 +24,12 @@ import { getUserGroups } from '../../../../api/users'; import Modal from '../../../../components/controls/Modal'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; /*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */ /*:: import type { Organization, OrgGroup } from '../../../../store/organizations/duck'; */ /*:: type Props = { + onClose: () => void; member: Member, organization: Organization, organizationGroups: Array<OrgGroup>, @@ -39,7 +39,6 @@ type Props = { /*:: type State = { - open: boolean, userGroups?: {}, loading?: boolean }; @@ -48,28 +47,17 @@ type State = { export default class ManageMemberGroupsForm extends React.PureComponent { /*:: mounted: boolean */ /*:: props: Props; */ - - state /*: State */ = { - open: false - }; + state /*: State */ = {}; componentDidMount() { this.mounted = true; + this.loadUserGroups(); } componentWillUnmount() { this.mounted = false; } - openForm = () => { - this.loadUserGroups(); - this.setState({ open: true }); - }; - - closeForm = () => { - this.setState({ open: false }); - }; - loadUserGroups = () => { this.setState({ loading: true }); getUserGroups(this.props.member.login, this.props.organization.key).then( @@ -119,13 +107,13 @@ export default class ManageMemberGroupsForm extends React.PureComponent { Object.keys(pickBy(this.state.userGroups, group => group.status === 'add')), Object.keys(pickBy(this.state.userGroups, group => group.status === 'remove')) ); - this.closeForm(); + this.props.onClose(); }; - renderModal() { + render() { const header = translate('organization.members.manage_groups'); return ( - <Modal key="manage-member-modal" contentLabel={header} onRequestClose={this.closeForm}> + <Modal contentLabel={header} onRequestClose={this.props.onClose}> <header className="modal-head"> <h2>{header}</h2> </header> @@ -142,9 +130,9 @@ export default class ManageMemberGroupsForm extends React.PureComponent { <ul className="list-spaced"> {this.props.organizationGroups.map(group => ( <OrganizationGroupCheckbox - key={group.id} - group={group} checked={this.isGroupSelected(group.name)} + group={group} + key={group.id} onCheck={this.onCheck} /> ))} @@ -154,7 +142,7 @@ export default class ManageMemberGroupsForm extends React.PureComponent { <footer className="modal-foot"> <div> <button type="submit">{translate('save')}</button> - <button type="reset" className="button-link" onClick={this.closeForm}> + <button className="button-link" onClick={this.props.onClose} type="reset"> {translate('cancel')} </button> </div> @@ -163,16 +151,4 @@ export default class ManageMemberGroupsForm extends React.PureComponent { </Modal> ); } - - render() { - const buttonComponent = ( - <ActionsDropdownItem onClick={this.openForm}> - {translate('organization.members.manage_groups')} - </ActionsDropdownItem> - ); - if (this.state.open) { - return [buttonComponent, this.renderModal()]; - } - return buttonComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js index d4dae2fa781..343054e6be3 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js @@ -20,69 +20,48 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; /*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */ /*:: import type { Organization } from '../../../../store/organizations/duck'; */ /*:: type Props = { + onClose: () => void; member: Member, organization: Organization, removeMember: (member: Member) => void }; */ -/*:: -type State = { - open: boolean -}; -*/ - export default class RemoveMemberForm extends React.PureComponent { /*:: props: Props; */ - - state /*: State */ = { - open: false - }; - - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - this.setState({ open: false }); - }; - handleSubmit = (e /*: Object */) => { e.preventDefault(); this.props.removeMember(this.props.member); - this.closeForm(); + this.props.onClose(); }; - renderModal() { + render() { const header = translate('users.remove'); return ( - <Modal key="remove-member-modal" contentLabel={header} onRequestClose={this.closeForm}> + <Modal contentLabel={header} key="remove-member-modal" onRequestClose={this.props.onClose}> <header className="modal-head"> <h2>{header}</h2> </header> <form onSubmit={this.handleSubmit}> - <div className="modal-body markdown"> - <p> - {translateWithParameters( - 'organization.members.remove_x', - this.props.member.name, - this.props.organization.name - )} - </p> + <div className="modal-body"> + {translateWithParameters( + 'organization.members.remove_x', + this.props.member.name, + this.props.organization.name + )} </div> <footer className="modal-foot"> <div> - <button type="submit" className="button-red" autoFocus={true}> + <button autoFocus={true} className="button-red" type="submit"> {translate('remove')} </button> - <button type="reset" className="button-link" onClick={this.closeForm}> + <button className="button-link" onClick={this.props.onClose} type="reset"> {translate('cancel')} </button> </div> @@ -91,16 +70,4 @@ export default class RemoveMemberForm extends React.PureComponent { </Modal> ); } - - render() { - const buttonComponent = ( - <ActionsDropdownItem destructive={true} onClick={this.openForm}> - {translate('organization.members.remove')} - </ActionsDropdownItem> - ); - if (this.state.open) { - return [buttonComponent, this.renderModal()]; - } - return buttonComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js index 2bd5905184b..c2d30153c74 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js @@ -54,19 +54,19 @@ function getMountedForm(updateFunc = jest.fn()) { const wrapper = shallow( <ManageMemberGroupsForm member={member} + onClose={jest.fn()} organization={organization} organizationGroups={organizationGroups} updateMemberGroups={updateFunc} - /> + />, + { disableLifecycleMethods: true } ); const instance = wrapper.instance(); - instance.loadUserGroups = jest.fn(() => { - instance.setState({ loading: false, userGroups }); - }); + wrapper.setState({ loading: false, userGroups }); return { wrapper, instance }; } -it('should render and open the modal', () => { +it('should render', () => { const wrapper = shallow( <ManageMemberGroupsForm member={member} @@ -76,21 +76,10 @@ it('should render and open the modal', () => { /> ); expect(wrapper).toMatchSnapshot(); - wrapper.setState({ open: true }); - expect(wrapper.first().getElements()).toMatchSnapshot(); -}); - -it('should correctly handle user interactions', () => { - const form = getMountedForm(); - form.wrapper.find('ActionsDropdownItem').prop('onClick')(); - expect(form.wrapper.state('open')).toBeTruthy(); - expect(form.instance.loadUserGroups).toBeCalled(); - expect(form.wrapper.state()).toMatchSnapshot(); }); it('should correctly select the groups', () => { const form = getMountedForm(); - form.instance.openForm(mockEvent); expect(form.instance.isGroupSelected(11)).toBeTruthy(); expect(form.instance.isGroupSelected(7)).toBeFalsy(); form.instance.onCheck(11, false); @@ -103,21 +92,9 @@ it('should correctly select the groups', () => { it('should correctly handle the submit event and close the modal', () => { const updateMemberGroups = jest.fn(); const form = getMountedForm(updateMemberGroups); - form.instance.openForm(mockEvent); form.instance.onCheck(11, false); form.instance.onCheck(7, true); form.instance.handleSubmit(mockEvent); expect(updateMemberGroups.mock.calls).toMatchSnapshot(); expect(form.wrapper.state()).toMatchSnapshot(); }); - -it('should reset the selected groups when the modal is opened', () => { - const form = getMountedForm(); - form.instance.openForm(mockEvent); - form.instance.onCheck(11, false); - form.instance.onCheck(7, true); - expect(form.wrapper.state()).toMatchSnapshot(); - form.instance.closeForm(); - form.instance.openForm(mockEvent); - expect(form.wrapper.state()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js index e02af92237a..fca12f0187b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js @@ -22,30 +22,31 @@ import { shallow } from 'enzyme'; import { click, mockEvent } from '../../../../../helpers/testUtils'; import RemoveMemberForm from '../RemoveMemberForm'; -jest.mock('react-dom'); - const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }; const organization = { name: 'MyOrg' }; -it('should render and open the modal', () => { +it('should render ', () => { const wrapper = shallow( - <RemoveMemberForm member={member} removeMember={jest.fn()} organization={organization} /> + <RemoveMemberForm member={member} organization={organization} removeMember={jest.fn()} /> ); expect(wrapper).toMatchSnapshot(); - wrapper.setState({ open: true }); - expect(wrapper.first().getElements()).toMatchSnapshot(); }); it('should correctly handle user interactions', () => { const removeMember = jest.fn(); const wrapper = shallow( - <RemoveMemberForm member={member} removeMember={removeMember} organization={organization} /> + <RemoveMemberForm + member={member} + onClose={jest.fn()} + organization={organization} + removeMember={removeMember} + /> ); - const instance = wrapper.instance(); - wrapper.find('ActionsDropdownItem').prop('onClick')(); - expect(wrapper.state('open')).toBeTruthy(); - instance.handleSubmit(mockEvent); - expect(removeMember.mock.calls).toMatchSnapshot(); - instance.closeForm(); - expect(wrapper.state('open')).toBeFalsy(); + wrapper.instance().handleSubmit(mockEvent); + expect(removeMember).toBeCalledWith({ + avatar: '', + groupCount: 3, + login: 'admin', + name: 'Admin Istrator' + }); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap index 94ff9bb6705..2eea5b66085 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap @@ -22,7 +22,6 @@ Array [ exports[`should correctly handle the submit event and close the modal 2`] = ` Object { "loading": false, - "open": false, "userGroups": Object { "11": Object { "description": "Technical accounts", @@ -38,21 +37,6 @@ Object { } `; -exports[`should correctly handle user interactions 1`] = ` -Object { - "loading": false, - "open": true, - "userGroups": Object { - "11": Object { - "description": "Technical accounts", - "id": 11, - "name": "pull-request-analysers", - "selected": true, - }, - }, -} -`; - exports[`should correctly select the groups 1`] = ` Object { "11": Object { @@ -68,136 +52,48 @@ Object { } `; -exports[`should render and open the modal 1`] = ` -<ActionsDropdownItem - onClick={[Function]} +exports[`should render 1`] = ` +<Modal + contentLabel="organization.members.manage_groups" > - organization.members.manage_groups -</ActionsDropdownItem> -`; - -exports[`should render and open the modal 2`] = ` -Array [ - <ActionsDropdownItem - onClick={[Function]} + <header + className="modal-head" > - organization.members.manage_groups - </ActionsDropdownItem>, - <Modal - contentLabel="organization.members.manage_groups" - onRequestClose={[Function]} + <h2> + organization.members.manage_groups + </h2> + </header> + <form + onSubmit={[Function]} > - <header - className="modal-head" + <div + className="modal-body" > - <h2> - organization.members.manage_groups - </h2> - </header> - <form - onSubmit={[Function]} + <strong> + organization.members.members_groups.Admin Istrator + </strong> + + <i + className="spinner" + /> + </div> + <footer + className="modal-foot" > - <div - className="modal-body" - > - <strong> - organization.members.members_groups.Admin Istrator - </strong> - - <ul - className="list-spaced" + <div> + <button + type="submit" + > + save + </button> + <button + className="button-link" + type="reset" > - <OrganizationGroupCheckbox - checked={false} - group={ - Object { - "description": "", - "id": "7", - "membersCount": 12, - "name": "professionals", - } - } - onCheck={[Function]} - /> - <OrganizationGroupCheckbox - checked={false} - group={ - Object { - "description": "Technical accounts", - "id": "11", - "membersCount": 3, - "name": "pull-request-analysers", - } - } - onCheck={[Function]} - /> - <OrganizationGroupCheckbox - checked={false} - group={ - Object { - "description": "System administrators", - "id": "1", - "membersCount": 17, - "name": "sonar-administrators", - } - } - onCheck={[Function]} - /> - </ul> + cancel + </button> </div> - <footer - className="modal-foot" - > - <div> - <button - type="submit" - > - save - </button> - <button - className="button-link" - onClick={[Function]} - type="reset" - > - cancel - </button> - </div> - </footer> - </form> - </Modal>, -] -`; - -exports[`should reset the selected groups when the modal is opened 1`] = ` -Object { - "loading": false, - "open": true, - "userGroups": Object { - "11": Object { - "description": "Technical accounts", - "id": 11, - "name": "pull-request-analysers", - "selected": true, - "status": "remove", - }, - "7": Object { - "status": "add", - }, - }, -} -`; - -exports[`should reset the selected groups when the modal is opened 2`] = ` -Object { - "loading": false, - "open": true, - "userGroups": Object { - "11": Object { - "description": "Technical accounts", - "id": 11, - "name": "pull-request-analysers", - "selected": true, - }, - }, -} + </footer> + </form> +</Modal> `; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap index 861e8d98f02..eb235ae0e4a 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap @@ -1,77 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should correctly handle user interactions 1`] = ` -Array [ - Array [ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - }, - ], -] -`; - -exports[`should render and open the modal 1`] = ` -<ActionsDropdownItem - destructive={true} - onClick={[Function]} +exports[`should render 1`] = ` +<Modal + contentLabel="users.remove" + key="remove-member-modal" > - organization.members.remove -</ActionsDropdownItem> -`; - -exports[`should render and open the modal 2`] = ` -Array [ - <ActionsDropdownItem - destructive={true} - onClick={[Function]} + <header + className="modal-head" > - organization.members.remove - </ActionsDropdownItem>, - <Modal - contentLabel="users.remove" - onRequestClose={[Function]} + <h2> + users.remove + </h2> + </header> + <form + onSubmit={[Function]} > - <header - className="modal-head" + <div + className="modal-body" > - <h2> - users.remove - </h2> - </header> - <form - onSubmit={[Function]} + organization.members.remove_x.Admin Istrator.MyOrg + </div> + <footer + className="modal-foot" > - <div - className="modal-body markdown" - > - <p> - organization.members.remove_x.Admin Istrator.MyOrg - </p> + <div> + <button + autoFocus={true} + className="button-red" + type="submit" + > + remove + </button> + <button + className="button-link" + type="reset" + > + cancel + </button> </div> - <footer - className="modal-foot" - > - <div> - <button - autoFocus={true} - className="button-red" - type="submit" - > - remove - </button> - <button - className="button-link" - onClick={[Function]} - type="reset" - > - cancel - </button> - </div> - </footer> - </form> - </Modal>, -] + </footer> + </form> +</Modal> `; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css deleted file mode 100644 index fad51874d3d..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -.organization-switch { - display: inline-block; -} - -.organization-switch .dropdown-toggle { - display: flex; - align-items: center; - height: calc(4 * var(--gridSize)); - padding: 0 var(--gridSize); - border: 1px solid transparent; - border-radius: 2px; - box-sizing: border-box; - color: var(--baseFontColor) !important; - transition: all 0.3s ease; -} - -.organization-switch .dropdown-toggle:hover, -.organization-switch.open .dropdown-toggle { - border-color: var(--barBorderColor); - background-color: #fff; - box-shadow: var(--defaultShadow); -} - -.organization-switch.open .dropdown-toggle { - border-bottom-color: transparent; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.organization-switch .dropdown-menu { - min-width: 100%; - margin-left: -1px; -} - -.organization-switch .dropdown-menu > li > a { - padding-left: var(--gridSize); - padding-right: var(--gridSize); -} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx index 636dc6cf0a1..f9b1e0ddf81 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx @@ -24,7 +24,6 @@ import OrganizationNavigationMenu from './OrganizationNavigationMenu'; import * as theme from '../../../app/theme'; import ContextNavBar from '../../../components/nav/ContextNavBar'; import { Organization } from '../../../app/types'; -import './OrganizationNavigation.css'; interface Props { location: { pathname: string }; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx index 338494e7604..3b6f30e664f 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx @@ -48,71 +48,69 @@ export default function OrganizationNavigationAdministration({ location, organiz ); return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active: adminActive })} - id="organization-navigation-admin" - href="#" - onClick={onToggleClick}> - {translate('layout.settings')} - <DropdownIcon className="little-spacer-left" /> - </a> - <ul className="dropdown-menu"> - {extensions.map(extension => ( - <li key={extension.key}> - <Link - to={`/organizations/${organization.key}/extension/${extension.key}`} - activeClassName="active"> - {extension.name} - </Link> - </li> - ))} - <li> - <Link to={`/organizations/${organization.key}/groups`} activeClassName="active"> - {translate('user_groups.page')} - </Link> - </li> - <li> - <Link to={`/organizations/${organization.key}/permissions`} activeClassName="active"> - {translate('permissions.page')} - </Link> - </li> - <li> + <Dropdown + overlay={ + <ul className="menu"> + {extensions.map(extension => ( + <li key={extension.key}> <Link - to={`/organizations/${organization.key}/permission_templates`} - activeClassName="active"> - {translate('permission_templates')} - </Link> - </li> - <li> - <Link - to={`/organizations/${organization.key}/projects_management`} - activeClassName="active"> - {translate('projects_management')} - </Link> - </li> - <li> - <Link to={`/organizations/${organization.key}/webhooks`} activeClassName="active"> - {translate('webhooks.page')} + activeClassName="active" + to={`/organizations/${organization.key}/extension/${extension.key}`}> + {extension.name} </Link> </li> + ))} + <li> + <Link activeClassName="active" to={`/organizations/${organization.key}/groups`}> + {translate('user_groups.page')} + </Link> + </li> + <li> + <Link activeClassName="active" to={`/organizations/${organization.key}/permissions`}> + {translate('permissions.page')} + </Link> + </li> + <li> + <Link + activeClassName="active" + to={`/organizations/${organization.key}/permission_templates`}> + {translate('permission_templates')} + </Link> + </li> + <li> + <Link + activeClassName="active" + to={`/organizations/${organization.key}/projects_management`}> + {translate('projects_management')} + </Link> + </li> + <li> + <Link activeClassName="active" to={`/organizations/${organization.key}/webhooks`}> + {translate('webhooks.page')} + </Link> + </li> + <li> + <Link activeClassName="active" to={`/organizations/${organization.key}/edit`}> + {translate('edit')} + </Link> + </li> + {organization.canDelete && ( <li> - <Link to={`/organizations/${organization.key}/edit`} activeClassName="active"> - {translate('edit')} + <Link activeClassName="active" to={`/organizations/${organization.key}/delete`}> + {translate('delete')} </Link> </li> - {organization.canDelete && ( - <li> - <Link to={`/organizations/${organization.key}/delete`} activeClassName="active"> - {translate('delete')} - </Link> - </li> - )} - </ul> - </li> - )} + )} + </ul> + } + tagName="li"> + <a + className={classNames('dropdown-toggle', { active: adminActive })} + href="#" + id="organization-navigation-admin"> + {translate('layout.settings')} + <DropdownIcon className="little-spacer-left" /> + </a> </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx index 5151194ba9f..67eac961218 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx @@ -23,6 +23,7 @@ import * as classNames from 'classnames'; import { Organization } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; import Dropdown from '../../../components/controls/Dropdown'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; interface Props { location: { pathname: string }; @@ -40,31 +41,28 @@ export default function OrganizationNavigationExtensions({ location, organizatio ); return ( - <Dropdown> - {({ onToggleClick, open }) => ( - <li className={classNames('dropdown', { open })}> - <a - className={classNames('dropdown-toggle', { active })} - id="organization-navigation-more" - href="#" - onClick={onToggleClick}> - {translate('more')} - <i className="icon-dropdown little-spacer-left" /> - </a> - - <ul className="dropdown-menu"> - {extensions.map(extension => ( - <li key={extension.key}> - <Link - to={`/organizations/${organization.key}/extension/${extension.key}`} - activeClassName="active"> - {extension.name} - </Link> - </li> - ))} - </ul> - </li> - )} + <Dropdown + overlay={ + <ul className="menu"> + {extensions.map(extension => ( + <li key={extension.key}> + <Link + activeClassName="active" + to={`/organizations/${organization.key}/extension/${extension.key}`}> + {extension.name} + </Link> + </li> + ))} + </ul> + } + tagName="li"> + <a + className={classNames('dropdown-toggle', { active })} + href="#" + id="organization-navigation-more"> + {translate('more')} + <DropdownIcon className="little-spacer-left" /> + </a> </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx index c6bc236126a..df497ccd8fc 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { sortBy } from 'lodash'; import { Organization } from '../../../app/types'; import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; @@ -38,20 +37,19 @@ export default function OrganizationNavigationHeader({ organization, organizatio <header className="navbar-context-header"> <OrganizationAvatar organization={organization} /> {other.length ? ( - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('organization-switch', 'dropdown', { open })}> - <a className="dropdown-toggle" href="#" onClick={onToggleClick}> - {organization.name} - <DropdownIcon className="little-spacer-left" /> - </a> - <ul className="dropdown-menu"> - {sortBy(other, org => org.name.toLowerCase()).map(organization => ( - <OrganizationListItem key={organization.key} organization={organization} /> - ))} - </ul> - </div> - )} + <Dropdown + className="display-inline-block" + overlay={ + <ul className="menu"> + {sortBy(other, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + </ul> + }> + <a className="spacer-left link-base-color link-no-underline" href="#"> + {organization.name} + <DropdownIcon className="little-spacer-left" /> + </a> </Dropdown> ) : ( <span className="spacer-left">{organization.name}</span> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx index f68fab3ac9f..eb8baeef190 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx @@ -33,5 +33,5 @@ it('renders', () => { }} /> ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx index f6afd014d72..57d43dc4682 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx @@ -52,5 +52,5 @@ it('renders dropdown', () => { organizations={organizations} /> ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap index c7764bf1c8b..27d6f917f78 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap @@ -1,83 +1,84 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` -<li - className="dropdown" +<Dropdown + overlay={ + <ul + className="menu" + > + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/groups" + > + user_groups.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/permissions" + > + permissions.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/permission_templates" + > + permission_templates + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/projects_management" + > + projects_management + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/webhooks" + > + webhooks.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/foo/edit" + > + edit + </Link> + </li> + </ul> + } + tagName="li" > <a className="dropdown-toggle" href="#" id="organization-navigation-admin" - onClick={[Function]} > layout.settings <DropdownIcon className="little-spacer-left" /> </a> - <ul - className="dropdown-menu" - > - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/groups" - > - user_groups.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/permissions" - > - permissions.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/permission_templates" - > - permission_templates - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/projects_management" - > - projects_management - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/webhooks" - > - webhooks.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/foo/edit" - > - edit - </Link> - </li> - </ul> -</li> +</Dropdown> `; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap index 949bb39ff04..626ef846617 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap @@ -22,44 +22,43 @@ exports[`renders 1`] = ` `; exports[`renders dropdown 1`] = ` -<div - className="organization-switch dropdown" +<Dropdown + className="display-inline-block" + overlay={ + <ul + className="menu" + > + <OrganizationListItem + organization={ + Object { + "isAdmin": true, + "key": "org1", + "name": "org1", + "projectVisibility": "public", + } + } + /> + <OrganizationListItem + organization={ + Object { + "isAdmin": false, + "key": "org2", + "name": "org2", + "projectVisibility": "public", + } + } + /> + </ul> + } > <a - className="dropdown-toggle" + className="spacer-left link-base-color link-no-underline" href="#" - onClick={[Function]} > Foo <DropdownIcon className="little-spacer-left" /> </a> - <ul - className="dropdown-menu" - > - <OrganizationListItem - key="org1" - organization={ - Object { - "isAdmin": true, - "key": "org1", - "name": "org1", - "projectVisibility": "public", - } - } - /> - <OrganizationListItem - key="org2" - organization={ - Object { - "isAdmin": false, - "key": "org2", - "name": "org2", - "projectVisibility": "public", - } - } - /> - </ul> -</div> +</Dropdown> `; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx index bdc74e4d941..855b3201c0c 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx @@ -22,58 +22,20 @@ import MetaTagsSelector from './MetaTagsSelector'; import { setProjectTags } from '../../../api/components'; import { translate } from '../../../helpers/l10n'; import TagsList from '../../../components/tags/TagsList'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { Component } from '../../../app/types'; import { Button } from '../../../components/ui/buttons'; +import Dropdown from '../../../components/controls/Dropdown'; +import { PopupPlacement } from '../../../components/ui/popups'; interface Props { component: Component; onComponentChange: (changes: {}) => void; } -interface State { - popupOpen: boolean; - popupPosition: BubblePopupPosition; -} - -export default class MetaTags extends React.PureComponent<Props, State> { +export default class MetaTags extends React.PureComponent<Props> { card?: HTMLDivElement | null; tagsList?: HTMLElement | null; tagsSelector?: HTMLDivElement | null; - state: State = { popupOpen: false, popupPosition: { top: 0, right: 0 } }; - - componentDidMount() { - if (this.canUpdateTags() && this.tagsList && this.card) { - const buttonPos = this.tagsList.getBoundingClientRect(); - const cardPos = this.card.getBoundingClientRect(); - this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) }); - - window.addEventListener('keydown', this.handleKey, false); - window.addEventListener('click', this.handleOutsideClick, false); - } - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleKey); - window.removeEventListener('click', this.handleOutsideClick); - } - - handleKey = (evt: KeyboardEvent) => { - // Escape key - if (evt.keyCode === 27) { - this.setState({ popupOpen: false }); - } - }; - - handleOutsideClick = (evt: Event) => { - if (!this.tagsSelector || !this.tagsSelector.contains(evt.target as Node)) { - this.setState({ popupOpen: false }); - } - }; - - handleClick = () => { - this.setState(state => ({ popupOpen: !state.popupOpen })); - }; canUpdateTags = () => { const { configuration } = this.props.component; @@ -94,29 +56,29 @@ export default class MetaTags extends React.PureComponent<Props, State> { render() { const { key } = this.props.component; - const { popupOpen, popupPosition } = this.state; const tags = this.props.component.tags || []; if (this.canUpdateTags()) { return ( <div className="big-spacer-top overview-meta-tags" ref={card => (this.card = card)}> - <Button - className="button-link" - innerRef={tagsList => (this.tagsList = tagsList)} - onClick={this.handleClick} - stopPropagation={true}> - <TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} /> - </Button> - {popupOpen && ( - <div ref={tagsSelector => (this.tagsSelector = tagsSelector)}> + <Dropdown + closeOnClick={false} + closeOnClickOutside={true} + overlay={ <MetaTagsSelector - position={popupPosition} project={key} selectedTags={tags} setProjectTags={this.handleSetProjectTags} /> - </div> - )} + } + overlayPlacement={PopupPlacement.BottomLeft}> + <Button + className="button-link" + innerRef={tagsList => (this.tagsList = tagsList)} + stopPropagation={true}> + <TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} /> + </Button> + </Dropdown> </div> ); } else { diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx index 783688ebf7c..fcc2952d745 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx @@ -20,11 +20,9 @@ import * as React from 'react'; import { without, difference } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { searchProjectTags } from '../../../api/components'; interface Props { - position: BubblePopupPosition; project: string; selectedTags: string[]; setProjectTags: (tags: string[]) => void; @@ -78,7 +76,6 @@ export default class MetaTagsSelector extends React.PureComponent<Props, State> onSearch={this.onSearch} onSelect={this.onSelect} onUnselect={this.onUnselect} - position={this.props.position} selectedTags={this.props.selectedTags} tags={availableTags} /> diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx index 7fa2003a331..b2ed2b1ac1e 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../../helpers/testUtils'; import MetaTags from '../MetaTags'; const component = { @@ -61,21 +60,3 @@ it('should render with tags and admin rights', () => { }) ).toMatchSnapshot(); }); - -it('should open the tag selector on click', () => { - const wrapper = shallow( - <MetaTags component={componentWithTags} onComponentChange={jest.fn()} />, - { - disableLifecycleMethods: true - } - ); - expect(wrapper).toMatchSnapshot(); - - // open - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); - - // close - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx index aefeb8d6fb6..0c4c24c9b1b 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx @@ -38,26 +38,14 @@ it('searches tags on mount', () => { (searchProjectTags as jest.Mock).mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] }) ); - mount( - <MetaTagsSelector - position={{ top: 0, right: 0 }} - project="foo" - selectedTags={[]} - setProjectTags={jest.fn()} - /> - ); + mount(<MetaTagsSelector project="foo" selectedTags={[]} setProjectTags={jest.fn()} />); expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); }); it('selects and deselects tags', () => { const setProjectTags = jest.fn(); const wrapper = shallow( - <MetaTagsSelector - position={{ top: 0, right: 0 }} - project="foo" - selectedTags={['foo', 'bar']} - setProjectTags={setProjectTags} - /> + <MetaTagsSelector project="foo" selectedTags={['foo', 'bar']} setProjectTags={setProjectTags} /> ); const tagSelect: any = wrapper.find('TagsSelector'); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap index d0b063accf7..7eb0fedde2c 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap @@ -1,112 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should open the tag selector on click 1`] = ` -<div - className="big-spacer-top overview-meta-tags" -> - <Button - className="button-link" - innerRef={[Function]} - onClick={[Function]} - stopPropagation={true} - > - <TagsList - allowUpdate={true} - tags={ - Array [ - "foo", - "bar", - ] - } - /> - </Button> -</div> -`; - -exports[`should open the tag selector on click 2`] = ` -<div - className="big-spacer-top overview-meta-tags" -> - <Button - className="button-link" - innerRef={[Function]} - onClick={[Function]} - stopPropagation={true} - > - <TagsList - allowUpdate={true} - tags={ - Array [ - "foo", - "bar", - ] - } - /> - </Button> - <div> - <MetaTagsSelector - position={ - Object { - "right": 0, - "top": 0, - } - } - project="my-second-project" - selectedTags={ - Array [ - "foo", - "bar", - ] - } - setProjectTags={[Function]} - /> - </div> -</div> -`; - -exports[`should open the tag selector on click 3`] = ` -<div - className="big-spacer-top overview-meta-tags" -> - <Button - className="button-link" - innerRef={[Function]} - onClick={[Function]} - stopPropagation={true} - > - <TagsList - allowUpdate={true} - tags={ - Array [ - "foo", - "bar", - ] - } - /> - </Button> -</div> -`; - exports[`should render with tags and admin rights 1`] = ` <div className="big-spacer-top overview-meta-tags" > - <Button - className="button-link" - innerRef={[Function]} - onClick={[Function]} - stopPropagation={true} + <Dropdown + closeOnClick={false} + closeOnClickOutside={true} + overlay={ + <MetaTagsSelector + project="my-second-project" + selectedTags={ + Array [ + "foo", + "bar", + ] + } + setProjectTags={[Function]} + /> + } + overlayPlacement="bottom-left" > - <TagsList - allowUpdate={true} - tags={ - Array [ - "foo", - "bar", - ] - } - /> - </Button> + <Button + className="button-link" + innerRef={[Function]} + stopPropagation={true} + > + <TagsList + allowUpdate={true} + tags={ + Array [ + "foo", + "bar", + ] + } + /> + </Button> + </Dropdown> </div> `; diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 0d0dc6dbee9..99b68c86254 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -389,15 +389,11 @@ border: none; } -.overview-analysis-graph .bubble-popup { +.overview-analysis-graph-popup { opacity: 0.8; padding: 0; } -.overview-analysis-graph .bubble-popup-arrow { - top: 7px; -} - .overview-analysis-graph-tooltip { padding: 4px; pointer-events: none; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx index 121c008ce9a..7ef344a3b23 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { difference } from 'lodash'; +import DeleteForm from './DeleteForm'; import Form from './Form'; import { setDefaultPermissionTemplate, @@ -28,11 +29,10 @@ import { } from '../../../api/permissions'; import { PermissionTemplate } from '../../../app/types'; import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; import QualifierIcon from '../../../components/shared/QualifierIcon'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; -interface Props { +export interface Props { fromDetails?: boolean; organization?: { isDefault?: boolean; key: string }; permissionTemplate: PermissionTemplate; @@ -41,6 +41,7 @@ interface Props { } interface State { + deleteForm: boolean; updateModal: boolean; } @@ -51,7 +52,7 @@ export default class ActionsCell extends React.PureComponent<Props, State> { router: PropTypes.object }; - state: State = { updateModal: false }; + state: State = { deleteForm: false, updateModal: false }; componentDidMount() { this.mounted = true; @@ -81,8 +82,18 @@ export default class ActionsCell extends React.PureComponent<Props, State> { ); }; - handleDelete = (templateId: string) => { - return deletePermissionTemplate({ templateId }).then(() => { + handleDeleteClick = () => { + this.setState({ deleteForm: true }); + }; + + handleCloseDeleteForm = () => { + if (this.mounted) { + this.setState({ deleteForm: false }); + } + }; + + handleDeleteSubmit = () => { + return deletePermissionTemplate({ templateId: this.props.permissionTemplate.id }).then(() => { const pathname = this.props.organization ? `/organizations/${this.props.organization.key}/permission_templates` : '/permission_templates'; @@ -159,18 +170,30 @@ export default class ActionsCell extends React.PureComponent<Props, State> { : '/permission_templates'; return ( - <ActionsDropdown> - {this.renderSetDefaultsControl()} - - {!this.props.fromDetails && ( - <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}> - {translate('edit_permissions')} + <> + <ActionsDropdown> + {this.renderSetDefaultsControl()} + + {!this.props.fromDetails && ( + <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}> + {translate('edit_permissions')} + </ActionsDropdownItem> + )} + + <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}> + {translate('update_details')} </ActionsDropdownItem> - )} - <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}> - {translate('update_details')} - </ActionsDropdownItem> + {t.defaultFor.length === 0 && ( + <ActionsDropdownItem + className="js-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> + )} + </ActionsDropdown> + {this.state.updateModal && ( <Form confirmButtonText={translate('update_verb')} @@ -181,25 +204,14 @@ export default class ActionsCell extends React.PureComponent<Props, State> { /> )} - {t.defaultFor.length === 0 && ( - <ConfirmButton - confirmButtonText={translate('delete')} - confirmData={t.id} - isDestructive={true} - modalBody={translateWithParameters( - 'permission_template.do_you_want_to_delete_template_xxx', - t.name - )} - modalHeader={translate('permission_template.delete_confirm_title')} - onConfirm={this.handleDelete}> - {({ onClick }) => ( - <ActionsDropdownItem className="js-delete" destructive={true} onClick={onClick}> - {translate('delete')} - </ActionsDropdownItem> - )} - </ConfirmButton> + {this.state.deleteForm && ( + <DeleteForm + onClose={this.handleCloseDeleteForm} + onSubmit={this.handleDeleteSubmit} + permissionTemplate={t} + /> )} - </ActionsDropdown> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx new file mode 100644 index 00000000000..b89b76e1c31 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { PermissionTemplate } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + onClose: () => void; + onSubmit: () => Promise<void>; + permissionTemplate: PermissionTemplate; +} + +export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }: Props) { + const header = translate('permission_template.delete_confirm_title'); + + return ( + <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + {translateWithParameters( + 'permission_template.do_you_want_to_delete_template_xxx', + t.name + )} + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton className="button-red" disabled={submitting}> + {translate('delete')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx index b4c48de8bf8..a3ea017810c 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx @@ -17,23 +17,24 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; -import ActionsCell from '../ActionsCell'; +import ActionsCell, { Props } from '../ActionsCell'; const SAMPLE = { + createdAt: '2018-01-01', id: 'id', name: 'name', permissions: [], defaultFor: [] }; -function renderActionsCell(props) { +function renderActionsCell(props?: Partial<Props>) { return shallow( <ActionsCell permissionTemplate={SAMPLE} - topQualifiers={['TRK', 'VW']} refresh={() => true} + topQualifiers={['TRK', 'VW']} {...props} /> ); @@ -53,7 +54,7 @@ it('should not set default', () => { }); it('should display all qualifiers for default organization', () => { - const organization = { isDefault: true }; + const organization = { isDefault: true, key: 'org' }; const setDefault = renderActionsCell({ organization }).find('.js-set-default'); expect(setDefault.length).toBe(2); expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK'); @@ -61,7 +62,7 @@ it('should display all qualifiers for default organization', () => { }); it('should display only projects for custom organization', () => { - const organization = { isDefault: false }; + const organization = { isDefault: false, key: 'org' }; const setDefault = renderActionsCell({ organization }).find('.js-set-default'); expect(setDefault.length).toBe(1); expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK'); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js index cf17cc3b037..ed7b41bb49b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js @@ -19,15 +19,14 @@ */ // @flow import React from 'react'; -import classNames from 'classnames'; import GraphsTooltipsContent from './GraphsTooltipsContent'; import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication'; import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues'; import { DEFAULT_GRAPH } from '../utils'; -import BubblePopup from '../../../components/common/BubblePopup'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import { Popup, PopupPlacement } from '../../../components/ui/popups'; /*:: import type { Event, MeasureHistory } from '../types'; */ /*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */ @@ -88,17 +87,18 @@ export default class GraphsTooltips extends React.PureComponent { const { events, measuresHistory, tooltipIdx } = this.props; const top = 30; let left = this.props.tooltipPos + 60; - let customClass; + let placement = PopupPlacement.RightTop; if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) { left -= TOOLTIP_WIDTH; - customClass = 'bubble-popup-right'; + placement = PopupPlacement.LeftTop; } const tooltipContent = this.renderContent().filter(Boolean); const addSeparator = tooltipContent.length > 0; return ( - <BubblePopup - customClass={classNames(customClass, 'disabled-pointer-events')} - position={{ top, left, width: TOOLTIP_WIDTH }}> + <Popup + className="disabled-pointer-events" + placement={placement} + style={{ top, left, width: TOOLTIP_WIDTH }}> <div className="project-activity-graph-tooltip"> <div className="project-activity-graph-tooltip-title spacer-bottom"> <DateTimeFormatter date={this.props.selectedDate} /> @@ -125,7 +125,7 @@ export default class GraphsTooltips extends React.PureComponent { )} </table> </div> - </BubblePopup> + </Popup> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index 5324153a4e0..d34e8de5448 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -25,7 +25,8 @@ import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; import ActionsDropdown, { - ActionsDropdownDivider + ActionsDropdownDivider, + ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis } from '../types'; */ @@ -45,15 +46,61 @@ type Props = { selected: boolean, updateSelectedDate: Date => void }; + +type State = { + addEventForm: bool, + addVersionForm: bool, + removeAnalysisForm: bool +} */ export default class ProjectActivityAnalysis extends React.PureComponent { + mounted /*: boolean */ = false; /*:: props: Props; */ + state /*: State */ = { addEventForm: false, addVersionForm: false, removeAnalysisForm: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } handleClick = () => this.props.updateSelectedDate(this.props.analysis.date); stopPropagation = (e /*: Event */) => e.stopPropagation(); + handleRemoveAnalysisClick = () => { + this.setState({ removeAnalysisForm: true }); + }; + + closeRemoveAnalysisForm = () => { + if (this.mounted) { + this.setState({ removeAnalysisForm: false }); + } + }; + + handleAddEventClick = () => { + this.setState({ addEventForm: true }); + }; + + closeAddEventForm = () => { + if (this.mounted) { + this.setState({ addEventForm: false }); + } + }; + + handleAddVersionClick = () => { + this.setState({ addVersionForm: true }); + }; + + closeAddVersionForm = () => { + if (this.mounted) { + this.setState({ addVersionForm: false }); + } + }; + render() { const { analysis, isFirst, canAdmin } = this.props; const { date, events } = analysis; @@ -71,7 +118,6 @@ export default class ProjectActivityAnalysis extends React.PureComponent { })} data-date={date.valueOf()} onClick={this.handleClick} - role="listitem" tabIndex="0"> <div className="project-activity-time spacer-right"> <TimeTooltipFormatter className="text-middle" date={date} /> @@ -80,29 +126,53 @@ export default class ProjectActivityAnalysis extends React.PureComponent { {(canAddVersion || canAddEvent || canDeleteAnalyses) && ( <div className="project-activity-analysis-actions big-spacer-right"> - <ActionsDropdown menuPosition="left" small={true} toggleClassName="js-analysis-actions"> + <ActionsDropdown small={true} toggleClassName="js-analysis-actions"> {canAddVersion && ( - <AddEventForm - addEvent={this.props.addVersion} - analysis={analysis} - addEventButtonText="project_activity.add_version" - /> + <ActionsDropdownItem className="js-add-event" onClick={this.handleAddVersionClick}> + {translate('project_activity.add_version')} + </ActionsDropdownItem> )} {canAddEvent && ( - <AddEventForm - addEvent={this.props.addCustomEvent} - analysis={analysis} - addEventButtonText="project_activity.add_custom_event" - /> + <ActionsDropdownItem className="js-add-event" onClick={this.handleAddEventClick}> + {translate('project_activity.add_custom_event')} + </ActionsDropdownItem> )} {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ActionsDropdownDivider />} {canDeleteAnalyses && ( - <RemoveAnalysisForm - analysis={analysis} - deleteAnalysis={this.props.deleteAnalysis} - /> + <ActionsDropdownItem + className="js-delete-analysis" + destructive={true} + onClick={this.handleRemoveAnalysisClick}> + {translate('project_activity.delete_analysis')} + </ActionsDropdownItem> )} </ActionsDropdown> + + {this.state.addVersionForm && ( + <AddEventForm + addEvent={this.props.addVersion} + addEventButtonText="project_activity.add_version" + analysis={analysis} + onClose={this.closeAddVersionForm} + /> + )} + + {this.state.addEventForm && ( + <AddEventForm + addEvent={this.props.addCustomEvent} + addEventButtonText="project_activity.add_custom_event" + analysis={analysis} + onClose={this.closeAddEventForm} + /> + )} + + {this.state.removeAnalysisForm && ( + <RemoveAnalysisForm + analysis={analysis} + deleteAnalysis={this.props.deleteAnalysis} + onClose={this.closeRemoveAnalysisForm} + /> + )} </div> )} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap index c2723826cfe..3089427f9ac 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap @@ -1,9 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should not add separators if not needed 1`] = ` -<BubblePopup - customClass="bubble-popup-right disabled-pointer-events" - position={ +<Popup + className="disabled-pointer-events" + placement="left-top" + style={ Object { "left": 476, "top": 30, @@ -32,13 +33,14 @@ exports[`should not add separators if not needed 1`] = ` /> </table> </div> -</BubblePopup> +</Popup> `; exports[`should render correctly for issues graphs 1`] = ` -<BubblePopup - customClass="bubble-popup-right disabled-pointer-events" - position={ +<Popup + className="disabled-pointer-events" + placement="left-top" + style={ Object { "left": 476, "top": 30, @@ -90,13 +92,14 @@ exports[`should render correctly for issues graphs 1`] = ` </tbody> </table> </div> -</BubblePopup> +</Popup> `; exports[`should render correctly for random graphs 1`] = ` -<BubblePopup - customClass="bubble-popup-right disabled-pointer-events" - position={ +<Popup + className="disabled-pointer-events" + placement="left-top" + style={ Object { "left": 476, "top": 30, @@ -142,5 +145,5 @@ exports[`should render correctly for random graphs 1`] = ` </tbody> </table> </div> -</BubblePopup> +</Popup> `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js index b47c72d926b..ded7c2b733c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate } from '../../../../helpers/l10n'; /*:: import type { Analysis } from '../../types'; */ @@ -28,13 +27,13 @@ import { translate } from '../../../../helpers/l10n'; type Props = { addEvent: (analysis: string, name: string, category?: string) => Promise<*>, analysis: Analysis, - addEventButtonText: string + addEventButtonText: string, + onClose: () => void; }; */ /*:: type State = { - open: boolean, processing: boolean, name: string }; @@ -44,7 +43,6 @@ export default class AddEventForm extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { - open: false, processing: false, name: '' }; @@ -57,16 +55,6 @@ export default class AddEventForm extends React.PureComponent { this.mounted = false; } - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - if (this.mounted) { - this.setState({ open: false, name: '' }); - } - }; - changeInput = (e /*: Object */) => { if (this.mounted) { this.setState({ name: e.target.value }); @@ -79,24 +67,18 @@ export default class AddEventForm extends React.PureComponent { } }; - stopProcessingAndClose = () => { - if (this.mounted) { - this.setState({ open: false, processing: false, name: '' }); - } - }; - handleSubmit = (e /*: Object */) => { e.preventDefault(); this.setState({ processing: true }); this.props .addEvent(this.props.analysis.key, this.state.name) - .then(this.stopProcessingAndClose, this.stopProcessing); + .then(this.props.onClose, this.stopProcessing); }; - renderModal() { + render() { const header = translate(this.props.addEventButtonText); return ( - <Modal key="add-event-modal" contentLabel={header} onRequestClose={this.closeForm}> + <Modal contentLabel={header} key="add-event-modal" onRequestClose={this.props.onClose}> <header className="modal-head"> <h2>{header}</h2> </header> @@ -106,11 +88,11 @@ export default class AddEventForm extends React.PureComponent { <div className="modal-field"> <label>{translate('name')}</label> <input - value={this.state.name} autoFocus={true} disabled={this.state.processing} - type="text" onChange={this.changeInput} + type="text" + value={this.state.name} /> </div> </div> @@ -121,7 +103,7 @@ export default class AddEventForm extends React.PureComponent { ) : ( <div> <button type="submit">{translate('save')}</button> - <button type="reset" className="button-link" onClick={this.closeForm}> + <button className="button-link" onClick={this.props.onClose} type="reset"> {translate('cancel')} </button> </div> @@ -131,16 +113,4 @@ export default class AddEventForm extends React.PureComponent { </Modal> ); } - - render() { - const linkComponent = ( - <ActionsDropdownItem className="js-add-event" onClick={this.openForm}> - {translate(this.props.addEventButtonText)} - </ActionsDropdownItem> - ); - if (this.state.open) { - return [linkComponent, this.renderModal()]; - } - return linkComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js index 78b47814b1b..3bf12702367 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js @@ -22,7 +22,8 @@ import React from 'react'; import { find, sortBy } from 'lodash'; import AddGraphMetricPopup from './AddGraphMetricPopup'; import DropdownIcon from '../../../../components/icons-components/DropdownIcon'; -import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; +import Dropdown from '../../../../components/controls/Dropdown'; +import { Button } from '../../../../components/ui/buttons'; import { isDiffMetric } from '../../../../helpers/measures'; import { getLocalizedMetricName, translate } from '../../../../helpers/l10n'; /*:: import type { Metric } from '../../types'; */ @@ -40,7 +41,6 @@ type Props = { /*:: type State = { - open: boolean, query: string, }; */ @@ -48,7 +48,6 @@ type State = { export default class AddGraphMetric extends React.PureComponent { /*:: props: Props; */ state /*: State */ = { - open: false, metrics: [], query: '', selectedMetrics: [] @@ -107,12 +106,6 @@ export default class AddGraphMetric extends React.PureComponent { return metric === undefined ? key : getLocalizedMetricName(metric); }; - toggleForm = () => { - this.setState(state => { - return { open: !state.open }; - }); - }; - onSearch = (query /*: string */) => { this.setState({ query }); return Promise.resolve(); @@ -138,10 +131,6 @@ export default class AddGraphMetric extends React.PureComponent { }); }; - togglePopup = (open /*: boolean*/) => { - this.setState({ open }); - }; - render() { const { query } = this.state; const filteredMetrics = this.filterMetricsElements(this.props, query); @@ -151,34 +140,27 @@ export default class AddGraphMetric extends React.PureComponent { query ); return ( - <div className="display-inline-block"> - <BubblePopupHelper - isOpen={this.state.open} - offset={{ horizontal: 16, vertical: 0 }} - popup={ - <AddGraphMetricPopup - elements={filteredMetrics} - filterSelected={this.filterSelected} - metricsTypeFilter={this.props.metricsTypeFilter} - onSearch={this.onSearch} - onSelect={this.onSelect} - onUnselect={this.onUnselect} - renderLabel={element => this.getLocalizedMetricNameFromKey(element)} - selectedElements={selectedMetrics} - /> - } - position="bottomright" - togglePopup={this.togglePopup}> - <button className="spacer-left" onClick={this.toggleForm} type="button"> - <span> - <span className="text-ellipsis spacer-right"> - {translate('project_activity.graphs.custom.add')} - </span> - <DropdownIcon className="vertical-text-top" /> - </span> - </button> - </BubblePopupHelper> - </div> + <Dropdown + className="display-inline-block" + overlay={ + <AddGraphMetricPopup + elements={filteredMetrics} + filterSelected={this.filterSelected} + metricsTypeFilter={this.props.metricsTypeFilter} + onSearch={this.onSearch} + onSelect={this.onSelect} + onUnselect={this.onUnselect} + renderLabel={element => this.getLocalizedMetricNameFromKey(element)} + selectedElements={selectedMetrics} + /> + }> + <Button className="spacer-left"> + <span className="text-ellipsis text-middle"> + {translate('project_activity.graphs.custom.add')} + </span> + <DropdownIcon className="text-top little-spacer-left" /> + </Button> + </Dropdown> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx index 81864bf8920..d4bf8bbd2e7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BubblePopup from '../../../../components/common/BubblePopup'; import MultiSelect from '../../../../components/common/MultiSelect'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; @@ -58,9 +57,7 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr } return ( - <BubblePopup - customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300" - position={props.popupPosition}> + <div className="menu abs-width-300"> <MultiSelect allowNewElements={false} allowSelection={props.selectedElements.length < 6} @@ -74,6 +71,6 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr renderLabel={props.renderLabel} selectedElements={props.selectedElements} /> - </BubblePopup> + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js index 47125ed09fe..e813985139a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js @@ -20,20 +20,19 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate } from '../../../../helpers/l10n'; /*:: import type { Analysis } from '../../types'; */ /*:: type Props = { analysis: Analysis, - deleteAnalysis: (analysis: string) => Promise<*> + deleteAnalysis: (analysis: string) => Promise<*>, + onClose: () => void; }; */ /*:: type State = { - open: boolean, processing: boolean }; */ @@ -42,7 +41,6 @@ export default class RemoveAnalysisForm extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { - open: false, processing: false }; @@ -54,40 +52,24 @@ export default class RemoveAnalysisForm extends React.PureComponent { this.mounted = false; } - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - if (this.mounted) { - this.setState({ open: false }); - } - }; - stopProcessing = () => { if (this.mounted) { this.setState({ processing: false }); } }; - stopProcessingAndClose = () => { - if (this.mounted) { - this.setState({ open: false, processing: false }); - } - }; - handleSubmit = (e /*: Event */) => { e.preventDefault(); this.setState({ processing: true }); this.props .deleteAnalysis(this.props.analysis.key) - .then(this.stopProcessingAndClose, this.stopProcessing); + .then(this.props.onClose, this.stopProcessing); }; - renderModal() { + render() { const header = translate('project_activity.delete_analysis'); return ( - <Modal key="delete-analysis-modal" contentLabel={header} onRequestClose={this.closeForm}> + <Modal contentLabel={header} key="delete-analysis-modal" onRequestClose={this.props.onClose}> <header className="modal-head"> <h2>{header}</h2> </header> @@ -100,10 +82,10 @@ export default class RemoveAnalysisForm extends React.PureComponent { <i className="spinner" /> ) : ( <div> - <button type="submit" className="button-red" autoFocus={true}> + <button autoFocus={true} className="button-red" type="submit"> {translate('delete')} </button> - <button type="reset" className="button-link" onClick={this.closeForm}> + <button className="button-link" onClick={this.props.onClose} type="reset"> {translate('cancel')} </button> </div> @@ -113,19 +95,4 @@ export default class RemoveAnalysisForm extends React.PureComponent { </Modal> ); } - - render() { - const linkComponent = ( - <ActionsDropdownItem - className="js-delete-analysis" - destructive={true} - onClick={this.openForm}> - {translate('project_activity.delete_analysis')} - </ActionsDropdownItem> - ); - if (this.state.open) { - return [linkComponent, this.renderModal()]; - } - return linkComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx index 6298f16e0a3..f0ec4ed6f3c 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx @@ -153,15 +153,17 @@ export default class App extends React.PureComponent<Props, State> { return ( <React.Fragment key={getBranchLikeKey(branchLike)}> {showOrphanHeader && ( - <li className="dropdown-header"> - <div className="display-inline-block text-middle"> - {translate('branches.orphan_branches')} - </div> - <HelpTooltip - className="spacer-left" - overlay={translate('branches.orphan_branches.tooltip')} - /> - </li> + <tr> + <td colSpan={4}> + <div className="display-inline-block text-middle"> + {translate('branches.orphan_branches')} + </div> + <HelpTooltip + className="spacer-left" + overlay={translate('branches.orphan_branches.tooltip')} + /> + </td> + </tr> )} <BranchRow branchLike={branchLike} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap index 5c8f4b20cf0..2770b226fdc 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap @@ -147,19 +147,21 @@ exports[`renders sorted list of branches 1`] = ` <React.Fragment key="branch-feature" > - <li - className="dropdown-header" - > - <div - className="display-inline-block text-middle" + <tr> + <td + colSpan={4} > - branches.orphan_branches - </div> - <HelpTooltip - className="spacer-left" - overlay="branches.orphan_branches.tooltip" - /> - </li> + <div + className="display-inline-block text-middle" + > + branches.orphan_branches + </div> + <HelpTooltip + className="spacer-left" + overlay="branches.orphan_branches.tooltip" + /> + </td> + </tr> <BranchRow branchLike={ Object { diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx index daa2f082aae..935ff76d795 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import * as classNames from 'classnames'; import { connect } from 'react-redux'; import * as PropTypes from 'prop-types'; import { sortBy } from 'lodash'; @@ -61,23 +60,19 @@ export class NoFavoriteProjects extends React.PureComponent<Props> { <a className="button" href="#" onClick={this.onAnalyzeProjectClick}> {translate('my_account.analyze_new_project')} </a> - <Dropdown> - {({ onToggleClick, open }) => ( - <div - className={classNames('display-inline-block', 'big-spacer-left', 'dropdown', { - open - })}> - <a className="button" href="#" onClick={onToggleClick}> - {translate('projects.no_favorite_projects.favorite_projects_from_orgs')} - <DropdownIcon className="little-spacer-left" /> - </a> - <ul className="dropdown-menu"> - {sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( - <OrganizationListItem key={organization.key} organization={organization} /> - ))} - </ul> - </div> - )} + <Dropdown + className="display-inline-block big-spacer-left" + overlay={ + <ul className="menu"> + {sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + </ul> + }> + <a className="button" href="#"> + {translate('projects.no_favorite_projects.favorite_projects_from_orgs')} + <DropdownIcon className="little-spacer-left" /> + </a> </Dropdown> <Link className="button big-spacer-left" to="/explore/projects"> {translate('projects.no_favorite_projects.favorite_public_projects')} @@ -90,7 +85,7 @@ export class NoFavoriteProjects extends React.PureComponent<Props> { {translate('projects.no_favorite_projects.engagement')} </p> <p className="big-spacer-top"> - <Link to="/projects/all" className="button"> + <Link className="button" to="/projects/all"> {translate('projects.explore_projects')} </Link> </p> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap index a7dede57348..8cf6f203496 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap @@ -52,7 +52,45 @@ exports[`renders for SonarCloud 1`] = ` > my_account.analyze_new_project </a> - <Dropdown /> + <Dropdown + className="display-inline-block big-spacer-left" + overlay={ + <ul + className="menu" + > + <OrganizationListItem + organization={ + Object { + "isAdmin": true, + "key": "org1", + "name": "org1", + "projectVisibility": "public", + } + } + /> + <OrganizationListItem + organization={ + Object { + "isAdmin": false, + "key": "org2", + "name": "org2", + "projectVisibility": "public", + } + } + /> + </ul> + } + > + <a + className="button" + href="#" + > + projects.no_favorite_projects.favorite_projects_from_orgs + <DropdownIcon + className="little-spacer-left" + /> + </a> + </Dropdown> <Link className="button big-spacer-left" onlyActiveOnIndex={false} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index 4a66fe59a8c..438db5c8a77 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -106,24 +106,28 @@ export default class ProjectRowActions extends React.PureComponent<Props, State> const { hasAccess } = this.state; return ( - <ActionsDropdown onOpen={this.handleDropdownOpen}> - {hasAccess === true && ( - <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}> - {translate('edit_permissions')} - </ActionsDropdownItem> - )} + <> + <ActionsDropdown onOpen={this.handleDropdownOpen}> + {hasAccess === true && ( + <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}> + {translate('edit_permissions')} + </ActionsDropdownItem> + )} + + {hasAccess === false && ( + <ActionsDropdownItem + className="js-restore-access" + onClick={this.handleRestoreAccessClick}> + {translate('global_permissions.restore_access')} + </ActionsDropdownItem> + )} - {hasAccess === false && ( <ActionsDropdownItem - className="js-restore-access" - onClick={this.handleRestoreAccessClick}> - {translate('global_permissions.restore_access')} + className="js-apply-template" + onClick={this.handleApplyTemplateClick}> + {translate('projects_role.apply_template')} </ActionsDropdownItem> - )} - - <ActionsDropdownItem className="js-apply-template" onClick={this.handleApplyTemplateClick}> - {translate('projects_role.apply_template')} - </ActionsDropdownItem> + </ActionsDropdown> {this.state.restoreAccessModal && ( <RestoreAccessModal @@ -141,7 +145,7 @@ export default class ProjectRowActions extends React.PureComponent<Props, State> project={this.props.project} /> )} - </ActionsDropdown> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx index b30e553d91d..df159be98da 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx @@ -44,7 +44,7 @@ it('restores access', async () => { const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); - wrapper.prop<Function>('onOpen')(); + wrapper.find('ActionsDropdown').prop<Function>('onOpen')(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap index 61ea2fee3de..cb6c9944857 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap @@ -18,53 +18,59 @@ exports[`applies permission template 1`] = ` `; exports[`restores access 1`] = ` -<ActionsDropdown - onOpen={[Function]} -> - <ActionsDropdownItem - className="js-apply-template" - onClick={[Function]} +<React.Fragment> + <ActionsDropdown + onOpen={[Function]} > - projects_role.apply_template - </ActionsDropdownItem> -</ActionsDropdown> + <ActionsDropdownItem + className="js-apply-template" + onClick={[Function]} + > + projects_role.apply_template + </ActionsDropdownItem> + </ActionsDropdown> +</React.Fragment> `; exports[`restores access 2`] = ` -<ActionsDropdown - onOpen={[Function]} -> - <ActionsDropdownItem - className="js-restore-access" - onClick={[Function]} +<React.Fragment> + <ActionsDropdown + onOpen={[Function]} > - global_permissions.restore_access - </ActionsDropdownItem> - <ActionsDropdownItem - className="js-apply-template" - onClick={[Function]} - > - projects_role.apply_template - </ActionsDropdownItem> -</ActionsDropdown> + <ActionsDropdownItem + className="js-restore-access" + onClick={[Function]} + > + global_permissions.restore_access + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-apply-template" + onClick={[Function]} + > + projects_role.apply_template + </ActionsDropdownItem> + </ActionsDropdown> +</React.Fragment> `; exports[`restores access 3`] = ` -<ActionsDropdown - onOpen={[Function]} -> - <ActionsDropdownItem - className="js-restore-access" - onClick={[Function]} - > - global_permissions.restore_access - </ActionsDropdownItem> - <ActionsDropdownItem - className="js-apply-template" - onClick={[Function]} +<React.Fragment> + <ActionsDropdown + onOpen={[Function]} > - projects_role.apply_template - </ActionsDropdownItem> + <ActionsDropdownItem + className="js-restore-access" + onClick={[Function]} + > + global_permissions.restore_access + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-apply-template" + onClick={[Function]} + > + projects_role.apply_template + </ActionsDropdownItem> + </ActionsDropdown> <RestoreAccessModal currentUser={ Object { @@ -84,5 +90,5 @@ exports[`restores access 3`] = ` } } /> -</ActionsDropdown> +</React.Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 018d35320f7..17dea8322e9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -138,58 +138,60 @@ export default class ProfileActions extends React.PureComponent<Props, State> { ); return ( - <ActionsDropdown className={this.props.className}> - {actions.edit && ( - <ActionsDropdownItem to={activateMoreUrl} id="quality-profile-activate-more-rules"> - {translate('quality_profiles.activate_more_rules')} - </ActionsDropdownItem> - )} - - {!profile.isBuiltIn && ( - <ActionsDropdownItem - download={`${profile.key}.xml`} - id="quality-profile-backup" - to={backupUrl}> - {translate('backup_verb')} - </ActionsDropdownItem> - )} - - <ActionsDropdownItem - id="quality-profile-compare" - to={getProfileComparePath(profile.name, profile.language, this.props.organization)}> - {translate('compare')} - </ActionsDropdownItem> - - {actions.copy && ( - <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}> - {translate('copy')} - </ActionsDropdownItem> - )} + <> + <ActionsDropdown className={this.props.className}> + {actions.edit && ( + <ActionsDropdownItem id="quality-profile-activate-more-rules" to={activateMoreUrl}> + {translate('quality_profiles.activate_more_rules')} + </ActionsDropdownItem> + )} + + {!profile.isBuiltIn && ( + <ActionsDropdownItem + download={`${profile.key}.xml`} + id="quality-profile-backup" + to={backupUrl}> + {translate('backup_verb')} + </ActionsDropdownItem> + )} - {actions.edit && ( - <ActionsDropdownItem id="quality-profile-rename" onClick={this.handleRenameClick}> - {translate('rename')} - </ActionsDropdownItem> - )} - - {actions.setAsDefault && ( <ActionsDropdownItem - id="quality-profile-set-as-default" - onClick={this.handleSetDefaultClick}> - {translate('set_as_default')} + id="quality-profile-compare" + to={getProfileComparePath(profile.name, profile.language, this.props.organization)}> + {translate('compare')} </ActionsDropdownItem> - )} - {actions.delete && <ActionsDropdownDivider />} - - {actions.delete && ( - <ActionsDropdownItem - destructive={true} - id="quality-profile-delete" - onClick={this.handleDeleteClick}> - {translate('delete')} - </ActionsDropdownItem> - )} + {actions.copy && ( + <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}> + {translate('copy')} + </ActionsDropdownItem> + )} + + {actions.edit && ( + <ActionsDropdownItem id="quality-profile-rename" onClick={this.handleRenameClick}> + {translate('rename')} + </ActionsDropdownItem> + )} + + {actions.setAsDefault && ( + <ActionsDropdownItem + id="quality-profile-set-as-default" + onClick={this.handleSetDefaultClick}> + {translate('set_as_default')} + </ActionsDropdownItem> + )} + + {actions.delete && <ActionsDropdownDivider />} + + {actions.delete && ( + <ActionsDropdownItem + destructive={true} + id="quality-profile-delete" + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> + )} + </ActionsDropdown> {this.state.copyFormOpen && ( <CopyProfileForm @@ -217,7 +219,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { profile={profile} /> )} - </ActionsDropdown> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index 01f38890790..86a903e0bef 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -1,139 +1,145 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders with all permissions 1`] = ` -<ActionsDropdown> - <ActionsDropdownItem - id="quality-profile-activate-more-rules" - to={ - Object { - "pathname": "/organizations/org/rules", - "query": Object { - "activation": "false", - "qprofile": "foo", - }, +<React.Fragment> + <ActionsDropdown> + <ActionsDropdownItem + id="quality-profile-activate-more-rules" + to={ + Object { + "pathname": "/organizations/org/rules", + "query": Object { + "activation": "false", + "qprofile": "foo", + }, + } } - } - > - quality_profiles.activate_more_rules - </ActionsDropdownItem> - <ActionsDropdownItem - download="foo.xml" - id="quality-profile-backup" - to="/api/qualityprofiles/backup?profileKey=foo" - > - backup_verb - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-compare" - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, + > + quality_profiles.activate_more_rules + </ActionsDropdownItem> + <ActionsDropdownItem + download="foo.xml" + id="quality-profile-backup" + to="/api/qualityprofiles/backup?profileKey=foo" + > + backup_verb + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-compare" + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } } - } - > - compare - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-copy" - onClick={[Function]} - > - copy - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-rename" - onClick={[Function]} - > - rename - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-set-as-default" - onClick={[Function]} - > - set_as_default - </ActionsDropdownItem> - <ActionsDropdownDivider /> - <ActionsDropdownItem - destructive={true} - id="quality-profile-delete" - onClick={[Function]} - > - delete - </ActionsDropdownItem> -</ActionsDropdown> + > + compare + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-copy" + onClick={[Function]} + > + copy + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-rename" + onClick={[Function]} + > + rename + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-set-as-default" + onClick={[Function]} + > + set_as_default + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + destructive={true} + id="quality-profile-delete" + onClick={[Function]} + > + delete + </ActionsDropdownItem> + </ActionsDropdown> +</React.Fragment> `; exports[`renders with no permissions 1`] = ` -<ActionsDropdown> - <ActionsDropdownItem - download="foo.xml" - id="quality-profile-backup" - to="/api/qualityprofiles/backup?profileKey=foo" - > - backup_verb - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-compare" - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, +<React.Fragment> + <ActionsDropdown> + <ActionsDropdownItem + download="foo.xml" + id="quality-profile-backup" + to="/api/qualityprofiles/backup?profileKey=foo" + > + backup_verb + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-compare" + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } } - } - > - compare - </ActionsDropdownItem> -</ActionsDropdown> + > + compare + </ActionsDropdownItem> + </ActionsDropdown> +</React.Fragment> `; exports[`renders with permission to edit only 1`] = ` -<ActionsDropdown> - <ActionsDropdownItem - id="quality-profile-activate-more-rules" - to={ - Object { - "pathname": "/organizations/org/rules", - "query": Object { - "activation": "false", - "qprofile": "foo", - }, +<React.Fragment> + <ActionsDropdown> + <ActionsDropdownItem + id="quality-profile-activate-more-rules" + to={ + Object { + "pathname": "/organizations/org/rules", + "query": Object { + "activation": "false", + "qprofile": "foo", + }, + } } - } - > - quality_profiles.activate_more_rules - </ActionsDropdownItem> - <ActionsDropdownItem - download="foo.xml" - id="quality-profile-backup" - to="/api/qualityprofiles/backup?profileKey=foo" - > - backup_verb - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-compare" - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, + > + quality_profiles.activate_more_rules + </ActionsDropdownItem> + <ActionsDropdownItem + download="foo.xml" + id="quality-profile-backup" + to="/api/qualityprofiles/backup?profileKey=foo" + > + backup_verb + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-compare" + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } } - } - > - compare - </ActionsDropdownItem> - <ActionsDropdownItem - id="quality-profile-rename" - onClick={[Function]} - > - rename - </ActionsDropdownItem> -</ActionsDropdown> + > + compare + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-rename" + onClick={[Function]} + > + rename + </ActionsDropdownItem> + </ActionsDropdown> +</React.Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx index 80ef81fbb54..8a0750085a4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { IndexLink } from 'react-router'; -import * as classNames from 'classnames'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getProfilesPath, getProfilesForLanguagePath } from '../utils'; import Dropdown from '../../../components/controls/Dropdown'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; interface Props { currentFilter?: string; @@ -48,36 +48,31 @@ export default function ProfilesListHeader({ currentFilter, languages, organizat return ( <header className="quality-profiles-list-header clearfix"> - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('dropdown', { open })}> - <a - className="dropdown-toggle link-no-underline js-language-filter" - href="#" - onClick={onToggleClick}> - {label} - <i className="icon-dropdown little-spacer-left" /> - </a> - - <ul className="dropdown-menu"> - <li> - <IndexLink to={getProfilesPath(organization)}> - {translate('quality_profiles.all_profiles')} + <Dropdown + className="display-inline-block" + overlay={ + <ul className="menu"> + <li> + <IndexLink to={getProfilesPath(organization)}> + {translate('quality_profiles.all_profiles')} + </IndexLink> + </li> + {languages.map(language => ( + <li key={language.key}> + <IndexLink + className="js-language-filter-option" + data-language={language.key} + to={getProfilesForLanguagePath(language.key, organization)}> + {language.name} </IndexLink> </li> - {languages.map(language => ( - <li key={language.key}> - <IndexLink - className="js-language-filter-option" - data-language={language.key} - to={getProfilesForLanguagePath(language.key, organization)}> - {language.name} - </IndexLink> - </li> - ))} - </ul> - </div> - )} + ))} + </ul> + }> + <a className="dropdown-toggle link-no-underline js-language-filter" href="#"> + <span className="text-middle">{label}</span> + <DropdownIcon className="little-spacer-left text-middle" /> + </a> </Dropdown> </header> ); diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index e34bbf7e10e..dd4c07d9b84 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import ChangeLogLevelForm from './ChangeLogLevelForm'; import RestartForm from '../../../components/common/RestartForm'; import { getFileNameSuffix } from '../utils'; @@ -102,53 +101,52 @@ export default class PageActions extends React.PureComponent<Props, State> { /> </span> {this.props.canDownloadLogs && ( - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('display-inline-block dropdown spacer-left', { open })}> - <Button onClick={onToggleClick}> - {translate('system.download_logs')} - <i className="icon-dropdown little-spacer-left" /> - </Button> - <ul className="dropdown-menu"> - <li> - <a - download="sonarqube_app.log" - href={logsUrl + '?process=app'} - id="logs-link" - target="_blank"> - Main Process - </a> - </li> - <li> - <a - download="sonarqube_ce.log" - href={logsUrl + '?process=ce'} - id="ce-logs-link" - target="_blank"> - Compute Engine - </a> - </li> - <li> - <a - download="sonarqube_es.log" - href={logsUrl + '?process=es'} - id="es-logs-link" - target="_blank"> - Search Engine - </a> - </li> - <li> - <a - download="sonarqube_web.log" - href={logsUrl + '?process=web'} - id="web-logs-link" - target="_blank"> - Web Server - </a> - </li> - </ul> - </div> - )} + <Dropdown + className="display-inline-block spacer-left" + overlay={ + <ul className="menu"> + <li> + <a + download="sonarqube_app.log" + href={logsUrl + '?process=app'} + id="logs-link" + target="_blank"> + Main Process + </a> + </li> + <li> + <a + download="sonarqube_ce.log" + href={logsUrl + '?process=ce'} + id="ce-logs-link" + target="_blank"> + Compute Engine + </a> + </li> + <li> + <a + download="sonarqube_es.log" + href={logsUrl + '?process=es'} + id="es-logs-link" + target="_blank"> + Search Engine + </a> + </li> + <li> + <a + download="sonarqube_web.log" + href={logsUrl + '?process=web'} + id="web-logs-link" + target="_blank"> + Web Server + </a> + </li> + </ul> + }> + <Button> + {translate('system.download_logs')} + <i className="icon-dropdown little-spacer-left" /> + </Button> </Dropdown> )} <a diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx index 44f9d93a7a8..6190c2ed029 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx @@ -29,7 +29,7 @@ jest.mock('../../utils', () => ({ it('should render correctly', () => { const wrapper = getWrapper({ serverId: 'MyServerId' }); expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should render without restart and log download', () => { diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap index 3c698c9ba19..14a070daf60 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap @@ -22,7 +22,62 @@ exports[`should render correctly 1`] = ` onClick={[Function]} /> </span> - <Dropdown /> + <Dropdown + className="display-inline-block spacer-left" + overlay={ + <ul + className="menu" + > + <li> + <a + download="sonarqube_app.log" + href="/api/system/logs?process=app" + id="logs-link" + target="_blank" + > + Main Process + </a> + </li> + <li> + <a + download="sonarqube_ce.log" + href="/api/system/logs?process=ce" + id="ce-logs-link" + target="_blank" + > + Compute Engine + </a> + </li> + <li> + <a + download="sonarqube_es.log" + href="/api/system/logs?process=es" + id="es-logs-link" + target="_blank" + > + Search Engine + </a> + </li> + <li> + <a + download="sonarqube_web.log" + href="/api/system/logs?process=web" + id="web-logs-link" + target="_blank" + > + Web Server + </a> + </li> + </ul> + } + > + <Button> + system.download_logs + <i + className="icon-dropdown little-spacer-left" + /> + </Button> + </Dropdown> <a className="button spacer-left" download="sonarqube-support-info-filesuffix(MyServerId).json" @@ -44,62 +99,62 @@ exports[`should render correctly 1`] = ` `; exports[`should render correctly 2`] = ` -<div - className="display-inline-block dropdown spacer-left" +<Dropdown + className="display-inline-block spacer-left" + overlay={ + <ul + className="menu" + > + <li> + <a + download="sonarqube_app.log" + href="/api/system/logs?process=app" + id="logs-link" + target="_blank" + > + Main Process + </a> + </li> + <li> + <a + download="sonarqube_ce.log" + href="/api/system/logs?process=ce" + id="ce-logs-link" + target="_blank" + > + Compute Engine + </a> + </li> + <li> + <a + download="sonarqube_es.log" + href="/api/system/logs?process=es" + id="es-logs-link" + target="_blank" + > + Search Engine + </a> + </li> + <li> + <a + download="sonarqube_web.log" + href="/api/system/logs?process=web" + id="web-logs-link" + target="_blank" + > + Web Server + </a> + </li> + </ul> + } > - <Button - onClick={[Function]} - > + <Button> system.download_logs <i className="icon-dropdown little-spacer-left" /> </Button> - <ul - className="dropdown-menu" - > - <li> - <a - download="sonarqube_app.log" - href="/api/system/logs?process=app" - id="logs-link" - target="_blank" - > - Main Process - </a> - </li> - <li> - <a - download="sonarqube_ce.log" - href="/api/system/logs?process=ce" - id="ce-logs-link" - target="_blank" - > - Compute Engine - </a> - </li> - <li> - <a - download="sonarqube_es.log" - href="/api/system/logs?process=es" - id="es-logs-link" - target="_blank" - > - Search Engine - </a> - </li> - <li> - <a - download="sonarqube_web.log" - href="/api/system/logs?process=web" - id="web-logs-link" - target="_blank" - > - Web Server - </a> - </li> - </ul> -</div> +</Dropdown> `; exports[`should render without restart and log download 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx index ccd1471a245..5c132436de1 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx @@ -49,7 +49,7 @@ export default class UserActions extends React.PureComponent<Props, State> { renderActions = () => { const { user } = this.props; return ( - <ActionsDropdown menuClassName="dropdown-menu-right"> + <ActionsDropdown> <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}> {translate('update_details')} </ActionsDropdownItem> diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap index 0fd31e7f6ae..9b8a3267c76 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap @@ -2,9 +2,7 @@ exports[`should render correctly 1`] = ` <React.Fragment> - <ActionsDropdown - menuClassName="dropdown-menu-right" - > + <ActionsDropdown> <ActionsDropdownItem className="js-user-update" onClick={[Function]} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx new file mode 100644 index 00000000000..4e177160af0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Webhook } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + onClose: () => void; + onSubmit: () => Promise<void>; + webhook: Webhook; +} + +export default function DeleteWebhookForm({ onClose, onSubmit, webhook }: Props) { + const header = translate('webhooks.delete'); + + return ( + <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + {translateWithParameters('webhooks.delete.confirm', webhook.name)} + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton className="button-red" disabled={submitting}> + {translate('delete')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx index 0027720ad60..cfbeff267e3 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import CreateWebhookForm from './CreateWebhookForm'; +import DeleteWebhookForm from './DeleteWebhookForm'; import DeliveriesForm from './DeliveriesForm'; import ActionsDropdown, { ActionsDropdownItem, ActionsDropdownDivider } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { Webhook } from '../../../app/types'; interface Props { @@ -35,13 +35,14 @@ interface Props { } interface State { + deleting: boolean; deliveries: boolean; updating: boolean; } export default class WebhookActions extends React.PureComponent<Props, State> { mounted = false; - state: State = { deliveries: false, updating: false }; + state: State = { deleting: false, deliveries: false, updating: false }; componentDidMount() { this.mounted = true; @@ -55,6 +56,16 @@ export default class WebhookActions extends React.PureComponent<Props, State> { return this.props.onDelete(this.props.webhook.key); }; + handleDeleteClick = () => { + this.setState({ deleting: true }); + }; + + handleDeletingStop = () => { + if (this.mounted) { + this.setState({ deleting: false }); + } + }; + handleDeliveriesClick = () => { this.setState({ deliveries: true }); }; @@ -91,25 +102,18 @@ export default class WebhookActions extends React.PureComponent<Props, State> { </ActionsDropdownItem> )} <ActionsDropdownDivider /> - <ConfirmButton - confirmButtonText={translate('delete')} - isDestructive={true} - modalBody={translateWithParameters('webhooks.delete.confirm', webhook.name)} - modalHeader={translate('webhooks.delete')} - onConfirm={this.handleDelete}> - {({ onClick }) => ( - <ActionsDropdownItem - className="js-webhook-delete" - destructive={true} - onClick={onClick}> - {translate('delete')} - </ActionsDropdownItem> - )} - </ConfirmButton> + <ActionsDropdownItem + className="js-webhook-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> </ActionsDropdown> + {this.state.deliveries && ( <DeliveriesForm onClose={this.handleDeliveriesStop} webhook={webhook} /> )} + {this.state.updating && ( <CreateWebhookForm onClose={this.handleUpdatingStop} @@ -117,6 +121,14 @@ export default class WebhookActions extends React.PureComponent<Props, State> { webhook={webhook} /> )} + + {this.state.deleting && ( + <DeleteWebhookForm + onClose={this.handleDeletingStop} + onSubmit={this.handleDelete} + webhook={webhook} + /> + )} </> ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx index 70fe2bc5c77..8e1fd5d57f4 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx @@ -55,14 +55,9 @@ it('should display the update webhook form', () => { it('should display the delete webhook form', () => { const onDelete = jest.fn(() => Promise.resolve()); const wrapper = getWrapper({ onDelete }); - click( - wrapper - .find('ConfirmButton') - .dive() - .find('.js-webhook-delete') - ); - expect(wrapper.find('ConfirmButton').exists()).toBeTruthy(); - wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); + click(wrapper.find('.js-webhook-delete')); + expect(wrapper.find('DeleteWebhookForm').exists()).toBeTruthy(); + wrapper.find('DeleteWebhookForm').prop<Function>('onSubmit')(); expect(onDelete).lastCalledWith(webhook.key); }); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap index 7079ff776af..f72bc992de3 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap @@ -18,13 +18,13 @@ exports[`should display the deliveries form 1`] = ` webhooks.deliveries.show </ActionsDropdownItem> <ActionsDropdownDivider /> - <ConfirmButton - confirmButtonText="delete" - isDestructive={true} - modalBody="webhooks.delete.confirm.foo" - modalHeader="webhooks.delete" - onConfirm={[Function]} - /> + <ActionsDropdownItem + className="js-webhook-delete" + destructive={true} + onClick={[Function]} + > + delete + </ActionsDropdownItem> </ActionsDropdown> </React.Fragment> `; @@ -41,13 +41,13 @@ exports[`should render correctly 1`] = ` update_verb </ActionsDropdownItem> <ActionsDropdownDivider /> - <ConfirmButton - confirmButtonText="delete" - isDestructive={true} - modalBody="webhooks.delete.confirm.foo" - modalHeader="webhooks.delete" - onConfirm={[Function]} - /> + <ActionsDropdownItem + className="js-webhook-delete" + destructive={true} + onClick={[Function]} + > + delete + </ActionsDropdownItem> </ActionsDropdown> </React.Fragment> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index 041856e2426..1499a05acb0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -21,7 +21,6 @@ import { stringify } from 'querystring'; import * as React from 'react'; import { Link } from 'react-router'; import * as PropTypes from 'prop-types'; -import * as classNames from 'classnames'; import MeasuresOverlay from './components/MeasuresOverlay'; import { SourceViewerFile, BranchLike } from '../../app/types'; import QualifierIcon from '../shared/QualifierIcon'; @@ -29,6 +28,7 @@ import Dropdown from '../controls/Dropdown'; import FavoriteContainer from '../controls/FavoriteContainer'; import ListIcon from '../icons-components/ListIcon'; import { ButtonIcon } from '../ui/buttons'; +import { PopupPlacement } from '../ui/popups'; import { WorkspaceContext } from '../workspace/context'; import { getPathUrlAsString, @@ -128,56 +128,54 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State </div> </div> - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('dropdown source-viewer-header-actions', { open })}> - <ButtonIcon - className="js-actions" - onClick={onToggleClick} - tooltip={translate('component_viewer.more_actions')}> - <ListIcon /> - </ButtonIcon> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}> - {translate('component_viewer.show_details')} - </a> - {this.state.measuresOverlay && ( - <MeasuresOverlay - branchLike={this.props.branchLike} - onClose={this.handleMeasuresOverlayClose} - sourceViewerFile={this.props.sourceViewerFile} - /> - )} - </li> - <li> - <a - className="js-new-window" - href={getPathUrlAsString({ - pathname: '/component', - query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } - })} - target="_blank"> - {translate('component_viewer.new_window')} - </a> - </li> - {!workspace && ( - <li> - <a className="js-workspace" href="#" onClick={this.openInWorkspace}> - {translate('component_viewer.open_in_workspace')} - </a> - </li> - )} + <Dropdown + className="source-viewer-header-actions" + overlay={ + <ul className="menu"> + <li> + <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}> + {translate('component_viewer.show_details')} + </a> + </li> + <li> + <a + className="js-new-window" + href={getPathUrlAsString({ + pathname: '/component', + query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } + })} + target="_blank"> + {translate('component_viewer.new_window')} + </a> + </li> + {!workspace && ( <li> - <a className="js-raw-source" href={rawSourcesLink} target="_blank"> - {translate('component_viewer.show_raw_source')} + <a className="js-workspace" href="#" onClick={this.openInWorkspace}> + {translate('component_viewer.open_in_workspace')} </a> </li> - </ul> - </div> - )} + )} + <li> + <a className="js-raw-source" href={rawSourcesLink} target="_blank"> + {translate('component_viewer.show_raw_source')} + </a> + </li> + </ul> + } + overlayPlacement={PopupPlacement.BottomRight}> + <ButtonIcon className="js-actions"> + <ListIcon /> + </ButtonIcon> </Dropdown> + {this.state.measuresOverlay && ( + <MeasuresOverlay + branchLike={this.props.branchLike} + onClose={this.handleMeasuresOverlayClose} + sourceViewerFile={this.props.sourceViewerFile} + /> + )} + <div className="source-viewer-header-measures"> {isUnitTest && ( <div className="source-viewer-header-measure"> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx index 8e5aeca1727..618e258da8b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx @@ -22,8 +22,9 @@ import { groupBy } from 'lodash'; import * as PropTypes from 'prop-types'; import { getTests } from '../../../api/components'; import { BranchLike, SourceLine, TestCase } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import TestStatusIcon from '../../shared/TestStatusIcon'; +import { PopupPlacement } from '../../ui/popups'; import { WorkspaceContext } from '../../workspace/context'; import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; @@ -34,7 +35,6 @@ interface Props { componentKey: string; line: SourceLine; onClose: () => void; - popupPosition?: any; } interface State { @@ -114,53 +114,55 @@ export default class CoveragePopup extends React.PureComponent<Props, State> { }); return ( - <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}> - <div className="bubble-popup-title"> - {translate('source_viewer.covered')} - {!!line.conditions && ( - <div> - {'('} - {line.coveredConditions || '0'} - {' of '} - {line.conditions} {translate('source_viewer.conditions')} - {')'} - </div> + <DropdownOverlay placement={PopupPlacement.RightTop}> + <div className="abs-width-400"> + <h6 className="spacer-bottom"> + {translate('source_viewer.covered')} + {!!line.conditions && ( + <div> + {'('} + {line.coveredConditions || '0'} + {' of '} + {line.conditions} {translate('source_viewer.conditions')} + {')'} + </div> + )} + </h6> + {this.state.loading ? ( + <i className="spinner" /> + ) : ( + <> + {testFiles.length === 0 && + translate('source_viewer.tooltip.no_information_about_tests')} + {testFiles.map(testFile => ( + <div className="spacer-top text-ellipsis" key={testFile.file.key}> + <a + data-key={testFile.file.key} + href="#" + onClick={this.handleTestClick} + title={testFile.file.longName}> + <span>{collapsePath(testFile.file.longName)}</span> + </a> + <ul> + {testFile.tests.map(testCase => ( + <li + className="display-flex-center little-spacer-top" + key={testCase.id} + title={testCase.name}> + <TestStatusIcon className="spacer-right" status={testCase.status} /> + <div className="display-inline-block text-ellipsis">{testCase.name}</div> + {testCase.status !== 'SKIPPED' && ( + <span className="spacer-left note">{testCase.durationInMs}ms</span> + )} + </li> + ))} + </ul> + </div> + ))} + </> )} </div> - {this.state.loading ? ( - <i className="spinner" /> - ) : ( - <> - {testFiles.length === 0 && - translate('source_viewer.tooltip.no_information_about_tests')} - {testFiles.map(testFile => ( - <div className="bubble-popup-section" key={testFile.file.key}> - <a - data-key={testFile.file.key} - href="#" - onClick={this.handleTestClick} - title={testFile.file.longName}> - <span>{collapsePath(testFile.file.longName)}</span> - </a> - <ul className="bubble-popup-list"> - {testFile.tests.map(testCase => ( - <li - className="component-viewer-popup-test" - key={testCase.id} - title={testCase.name}> - <TestStatusIcon className="spacer-right" status={testCase.status} /> - {testCase.name} - {testCase.status !== 'SKIPPED' && ( - <span className="spacer-left note">{testCase.durationInMs}ms</span> - )} - </li> - ))} - </ul> - </div> - ))} - </> - )} - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx index 163648dd6e2..1322545356f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -22,8 +22,9 @@ import { Link } from 'react-router'; import * as PropTypes from 'prop-types'; import { groupBy, sortBy } from 'lodash'; import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import QualifierIcon from '../../shared/QualifierIcon'; +import { PopupPlacement } from '../../ui/popups'; import { WorkspaceContext } from '../../workspace/context'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; @@ -90,8 +91,8 @@ export default class DuplicationPopup extends React.PureComponent<Props> { ); return ( - <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}> - <div className="bubble-popup-container"> + <DropdownOverlay placement={PopupPlacement.RightTop}> + <div className="source-viewer-bubble-popup abs-width-400"> {this.props.inRemovedComponent && ( <div className="alert alert-warning"> {translate('duplications.dups_found_on_deleted_resource')} @@ -99,11 +100,11 @@ export default class DuplicationPopup extends React.PureComponent<Props> { )} {duplications.length > 0 && ( <> - <div className="bubble-popup-title"> + <h6 className="spacer-bottom"> {translate('component_viewer.transition.duplication')} - </div> + </h6> {duplications.map(duplication => ( - <div className="bubble-popup-section" key={duplication.file.key}> + <div className="spacer-top text-ellipsis" key={duplication.file.key}> <div className="component-name"> {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( <> @@ -164,7 +165,7 @@ export default class DuplicationPopup extends React.PureComponent<Props> { </> )} </div> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx index 17d98a03eca..be4c2d1a842 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import CoveragePopup from './CoveragePopup'; import { BranchLike, SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; +import Toggler from '../../controls/Toggler'; import { translate } from '../../../helpers/l10n'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { branchLike: BranchLike | undefined; @@ -59,7 +59,9 @@ export default class LineCoverage extends React.PureComponent<Props> { line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; const cell = line.coverageStatus ? ( - <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right"> + <Tooltip + overlay={popupOpen ? undefined : translate('source_viewer.tooltip', line.coverageStatus)} + placement="right"> <div className="source-line-bar" /> </Tooltip> ) : ( @@ -75,20 +77,19 @@ export default class LineCoverage extends React.PureComponent<Props> { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - {cell} - <BubblePopupHelper - isOpen={popupOpen} - popup={ + <Toggler + onRequestClose={this.closePopup} + open={popupOpen} + overlay={ <CoveragePopup branchLike={branchLike} componentKey={componentKey} line={line} onClose={this.closePopup} /> - } - position="bottomright" - togglePopup={this.handleTogglePopup} - /> + }> + {cell} + </Toggler> </td> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx index 0121bfa868d..93772dcbb72 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; +import Toggler from '../../controls/Toggler'; import { translate } from '../../../helpers/l10n'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { duplicated: boolean; @@ -54,6 +54,10 @@ export default class LineDuplicationBlock extends React.PureComponent<Props> { }); }; + closePopup = () => { + this.handleTogglePopup(false); + }; + render() { const { duplicated, index, line, popupOpen } = this.props; const className = classNames('source-meta', 'source-line-duplications-extra', { @@ -71,15 +75,14 @@ export default class LineDuplicationBlock extends React.PureComponent<Props> { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right"> - {cell} - </Tooltip> - <BubblePopupHelper - isOpen={popupOpen} - popup={this.props.renderDuplicationPopup(index, line.line)} - position="bottomright" - togglePopup={this.handleTogglePopup} - /> + <Toggler + onRequestClose={this.closePopup} + open={popupOpen} + overlay={this.props.renderDuplicationPopup(index, line.line)}> + <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right"> + {cell} + </Tooltip> + </Toggler> </td> ) : ( <td className={className} data-index={index} data-line-number={line.line}> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx index 9297036d88d..15af9ac258d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import LineOptionsPopup from './LineOptionsPopup'; import { BranchLike, SourceLine } from '../../../app/types'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; +import Toggler from '../../controls/Toggler'; interface Props { branchLike: BranchLike | undefined; @@ -42,6 +42,10 @@ export default class LineNumber extends React.PureComponent<Props> { this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open }); }; + closePopup = () => { + this.handleTogglePopup(false); + }; + render() { const { branchLike, componentKey, line, popupOpen } = this.props; const { line: lineNumber } = line; @@ -54,14 +58,12 @@ export default class LineNumber extends React.PureComponent<Props> { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - <BubblePopupHelper - isOpen={popupOpen} - offset={{ vertical: -18, horizontal: 0 }} - popup={ + <Toggler + onRequestClose={this.closePopup} + open={popupOpen} + overlay={ <LineOptionsPopup branchLike={branchLike} componentKey={componentKey} line={line} /> } - position="bottomright" - togglePopup={this.handleTogglePopup} /> </td> ) : ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx index 340304bb280..34a136d8391 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx @@ -20,7 +20,8 @@ import * as React from 'react'; import { Link } from 'react-router'; import { BranchLike, SourceLine } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; +import { PopupPlacement } from '../../ui/popups'; import { translate } from '../../../helpers/l10n'; import { getBranchLikeQuery } from '../../../helpers/branches'; @@ -28,21 +29,20 @@ interface Props { branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; - popupPosition?: any; } -export default function LineOptionsPopup({ branchLike, componentKey, line, popupPosition }: Props) { +export default function LineOptionsPopup({ branchLike, componentKey, line }: Props) { const permalink = { pathname: '/component', query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) } }; return ( - <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}> - <div className="bubble-popup-section"> + <DropdownOverlay placement={PopupPlacement.RightTop}> + <div className="source-viewer-bubble-popup nowrap"> <Link className="js-get-permalink" to={permalink}> {translate('component_viewer.get_permalink')} </Link> </div> - </BubblePopup> + </DropdownOverlay> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx index e692a3c46f1..83c326a12ea 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import SCMPopup from './SCMPopup'; import { SourceLine } from '../../../app/types'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; +import Toggler from '../../controls/Toggler'; interface Props { line: SourceLine; @@ -41,6 +41,10 @@ export default class LineSCM extends React.PureComponent<Props> { this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open }); }; + closePopup = () => { + this.handleTogglePopup(false); + }; + render() { const { line, popupOpen, previousLine } = this.props; const hasPopup = !!line.line; @@ -55,14 +59,12 @@ export default class LineSCM extends React.PureComponent<Props> { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - {cell} - <BubblePopupHelper - isOpen={popupOpen} - offset={{ vertical: -18, horizontal: 0 }} - popup={<SCMPopup line={line} />} - position="bottomright" - togglePopup={this.handleTogglePopup} - /> + <Toggler + onRequestClose={this.closePopup} + open={popupOpen} + overlay={<SCMPopup line={line} />}> + {cell} + </Toggler> </td> ) : ( <td className="source-meta source-line-scm" data-line-number={line.line}> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx index f89a9aa408b..1e46274b9e9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx @@ -81,12 +81,12 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Pro testCase.status !== 'FAILURE' && coveredFiles !== undefined && ( <> - <div className="bubble-popup-title"> + <h6 className="spacer-bottom"> {translate('component_viewer.transition.covers')} - </div> + </h6> {coveredFiles.length > 0 ? coveredFiles.map(coveredFile => ( - <div className="bubble-popup-section" key={coveredFile.key}> + <div className="spacer-top" key={coveredFile.key}> <Link to={getProjectUrl(coveredFile.key)}>{coveredFile.longName}</Link> <span className="note spacer-left"> {translateWithParameters( @@ -102,7 +102,7 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Pro {testCase.status !== 'OK' && ( <> - <div className="bubble-popup-title">{translate('component_viewer.details')}</div> + <h6 className="spacer-bottom">{translate('component_viewer.details')}</h6> {testCase.message && <pre>{testCase.message}</pre>} <pre>{testCase.stacktrace}</pre> </> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx index 1a94d48e657..fa1b766c12a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx @@ -19,24 +19,26 @@ */ import * as React from 'react'; import { SourceLine } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import DateFormatter from '../../intl/DateFormatter'; +import { PopupPlacement } from '../../ui/popups'; interface Props { line: SourceLine; - popupPosition?: any; } -export default function SCMPopup({ line, popupPosition }: Props) { +export default function SCMPopup({ line }: Props) { return ( - <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}> - <div className="bubble-popup-section">{line.scmAuthor}</div> - {line.scmDate && ( - <div className="bubble-popup-section"> - <DateFormatter date={line.scmDate} /> - </div> - )} - {line.scmRevision && <div className="bubble-popup-section">{line.scmRevision}</div>} - </BubblePopup> + <DropdownOverlay placement={PopupPlacement.RightTop}> + <div className="source-viewer-bubble-popup abs-width-400"> + <div>{line.scmAuthor}</div> + {line.scmDate && ( + <div className="spacer-top"> + <DateFormatter date={line.scmDate} /> + </div> + )} + {line.scmRevision && <div className="spacer-top">{line.scmRevision}</div>} + </div> + </DropdownOverlay> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap index 4e01802b363..e715b7ded52 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap @@ -8,17 +8,10 @@ exports[`render covered line 1`] = ` role="button" tabIndex={0} > - <Tooltip - overlay="source_viewer.tooltip.covered" - placement="right" - > - <div - className="source-line-bar" - /> - </Tooltip> - <BubblePopupHelper - isOpen={false} - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <CoveragePopup branchLike={undefined} componentKey="foo" @@ -31,9 +24,16 @@ exports[`render covered line 1`] = ` onClose={[Function]} /> } - position="bottomright" - togglePopup={[Function]} - /> + > + <Tooltip + overlay="source_viewer.tooltip.covered" + placement="right" + > + <div + className="source-line-bar" + /> + </Tooltip> + </Toggler> </td> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap index 9ea77716f15..7147eca868e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap @@ -9,19 +9,19 @@ exports[`render duplicated line 1`] = ` role="button" tabIndex={0} > - <Tooltip - overlay="source_viewer.tooltip.duplicated_block" - placement="right" + <Toggler + onRequestClose={[Function]} + open={false} > - <div - className="source-line-bar" - /> - </Tooltip> - <BubblePopupHelper - isOpen={false} - position="bottomright" - togglePopup={[Function]} - /> + <Tooltip + overlay="source_viewer.tooltip.duplicated_block" + placement="right" + > + <div + className="source-line-bar" + /> + </Tooltip> + </Toggler> </td> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap index 3841048e4ae..f5d3c9deff3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap @@ -14,15 +14,10 @@ exports[`render line 3 1`] = ` role="button" tabIndex={0} > - <BubblePopupHelper - isOpen={false} - offset={ - Object { - "horizontal": 0, - "vertical": -18, - } - } - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <LineOptionsPopup branchLike={undefined} componentKey="foo" @@ -33,8 +28,6 @@ exports[`render line 3 1`] = ` } /> } - position="bottomright" - togglePopup={[Function]} /> </td> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap index 13d7b6ee859..96b360bb80a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -<BubblePopup - customClass="source-viewer-bubble-popup" +<DropdownOverlay + placement="right-top" > <div - className="bubble-popup-section" + className="source-viewer-bubble-popup nowrap" > <Link className="js-get-permalink" @@ -25,5 +25,5 @@ exports[`should render 1`] = ` component_viewer.get_permalink </Link> </div> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap index c320b6a9b36..dac002460eb 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap @@ -8,15 +8,10 @@ exports[`does not render scm details 1`] = ` role="button" tabIndex={0} > - <BubblePopupHelper - isOpen={false} - offset={ - Object { - "horizontal": 0, - "vertical": -18, - } - } - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <SCMPopup line={ Object { @@ -27,8 +22,6 @@ exports[`does not render scm details 1`] = ` } /> } - position="bottomright" - togglePopup={[Function]} /> </td> `; @@ -41,19 +34,10 @@ exports[`render scm details 1`] = ` role="button" tabIndex={0} > - <div - className="source-line-scm-inner" - data-author="foo" - /> - <BubblePopupHelper - isOpen={false} - offset={ - Object { - "horizontal": 0, - "vertical": -18, - } - } - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <SCMPopup line={ Object { @@ -64,9 +48,12 @@ exports[`render scm details 1`] = ` } /> } - position="bottomright" - togglePopup={[Function]} - /> + > + <div + className="source-line-scm-inner" + data-author="foo" + /> + </Toggler> </td> `; @@ -78,19 +65,10 @@ exports[`render scm details for the first line 1`] = ` role="button" tabIndex={0} > - <div - className="source-line-scm-inner" - data-author="foo" - /> - <BubblePopupHelper - isOpen={false} - offset={ - Object { - "horizontal": 0, - "vertical": -18, - } - } - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <SCMPopup line={ Object { @@ -101,8 +79,11 @@ exports[`render scm details for the first line 1`] = ` } /> } - position="bottomright" - togglePopup={[Function]} - /> + > + <div + className="source-line-scm-inner" + data-author="foo" + /> + </Toggler> </td> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap index 43ac2de8f8e..b053a1bf9af 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap @@ -12,11 +12,11 @@ exports[`should render ERROR test 1`] = ` className="source-viewer-measures-card source-viewer-measures-card-fixed-height" > <React.Fragment> - <div - className="bubble-popup-title" + <h6 + className="spacer-bottom" > component_viewer.details - </div> + </h6> <pre> Something failed </pre> @@ -39,13 +39,13 @@ exports[`should render OK test 1`] = ` className="source-viewer-measures-card source-viewer-measures-card-fixed-height" > <React.Fragment> - <div - className="bubble-popup-title" + <h6 + className="spacer-bottom" > component_viewer.transition.covers - </div> + </h6> <div - className="bubble-popup-section" + className="spacer-top" key="project:src/file.js" > <Link diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap index d8460698802..99378cb457d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap @@ -1,20 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -<BubblePopup - customClass="source-viewer-bubble-popup" +<DropdownOverlay + placement="right-top" > <div - className="bubble-popup-section" + className="source-viewer-bubble-popup abs-width-400" > - foo + <div> + foo + </div> + <div + className="spacer-top" + > + <DateFormatter + date="2017-01-01" + /> + </div> </div> - <div - className="bubble-popup-section" - > - <DateFormatter - date="2017-01-01" - /> - </div> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css index 60a57ab69ba..839838acfe9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -396,10 +396,6 @@ clear: both; } -.source-viewer-measures .bubble-popup-section { - width: 100%; -} - .source-viewer-measures + .source-viewer-measures { margin-top: 40px; } @@ -508,9 +504,6 @@ } .source-viewer-bubble-popup { - top: -16px; - left: 100%; - width: 480px; font-family: var(--baseFontFamily); font-size: var(--baseFontSize); text-align: left; diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js index 2723b9dad7f..f4d54a0aa5f 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -251,24 +251,15 @@ export default class AdvancedTimeline extends React.PureComponent { handleMouseOut = (evt /*: Event & { relatedTarget: HTMLElement } */) => { const { updateTooltip } = this.props; - const targetClass = - evt.relatedTarget && typeof evt.relatedTarget.className === 'string' - ? evt.relatedTarget.className - : ''; - if ( - !updateTooltip || - targetClass.includes('bubble-popup') || - targetClass.includes('graph-tooltip') - ) { - return; + if (updateTooltip) { + this.setState({ + mouseOver: false, + selectedDate: null, + selectedDateXPos: null, + selectedDateIdx: null + }); + updateTooltip(null, null, null); } - this.setState({ - mouseOver: false, - selectedDate: null, - selectedDateXPos: null, - selectedDateIdx: null - }); - updateTooltip(null, null, null); }; handleClick = () => { diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx deleted file mode 100644 index 9f66d022cc9..00000000000 --- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import * as classNames from 'classnames'; -import { BubblePopupPosition } from './BubblePopup'; - -interface Props { - className?: string; - children?: React.ReactNode; - isOpen: boolean; - offset?: { vertical: number; horizontal: number }; - popup: JSX.Element; - position: 'bottomleft' | 'bottomright'; - togglePopup: (show: boolean) => void; -} - -interface State { - position: BubblePopupPosition; -} - -export default class BubblePopupHelper extends React.PureComponent<Props, State> { - container?: HTMLElement | null; - popupContainer?: HTMLElement | null; - state: State = { - position: { top: 0, right: 0 } - }; - - componentDidMount() { - this.setState({ position: this.getPosition(this.props) }); - } - - componentWillReceiveProps(nextProps: Props) { - if (!this.props.isOpen && nextProps.isOpen) { - window.addEventListener('keydown', this.handleKey, false); - window.addEventListener('click', this.handleOutsideClick, false); - } else if (this.props.isOpen && !nextProps.isOpen) { - window.removeEventListener('keydown', this.handleKey); - window.removeEventListener('click', this.handleOutsideClick); - } - } - - handleKey = (event: KeyboardEvent) => { - // Escape key - if (event.keyCode === 27) { - this.props.togglePopup(false); - } - }; - - handleOutsideClick = (event: MouseEvent) => { - if (!this.popupContainer || !this.popupContainer.contains(event.target as Node)) { - this.props.togglePopup(false); - } - }; - - handleClick(event: React.SyntheticEvent<HTMLElement>) { - event.stopPropagation(); - } - - getPosition(props: Props) { - if (this.container) { - const containerPos = this.container.getBoundingClientRect(); - const { position } = props; - const offset = props.offset || { vertical: 0, horizontal: 0 }; - if (position === 'bottomleft') { - return { top: containerPos.height + offset.vertical, left: offset.horizontal }; - } else { - // if (position === 'bottomright') - return { top: containerPos.height + offset.vertical, right: offset.horizontal }; - } - } else { - return { top: 0, right: 0 }; - } - } - - render() { - return ( - <div - className={classNames(this.props.className, 'bubble-popup-helper')} - onClick={this.handleClick} - ref={container => (this.container = container)} - role="tooltip" - tabIndex={0}> - {this.props.children} - {this.props.isOpen && ( - <div ref={popupContainer => (this.popupContainer = popupContainer)}> - {React.cloneElement(this.props.popup, { - popupPosition: this.state.position - })} - </div> - )} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js deleted file mode 100644 index ffd00e6cb84..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow, mount } from 'enzyme'; -import React from 'react'; -import BubblePopupHelper from '../BubblePopupHelper'; -import BubblePopup from '../BubblePopup'; -import { click } from '../../../helpers/testUtils'; - -it('should render an open popup on the right', () => { - const toggle = jest.fn(); - const popup = shallow( - <BubblePopupHelper - isOpen={true} - position="bottomright" - togglePopup={toggle} - popup={ - <BubblePopup> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper>, - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render the popup helper with a closed popup', () => { - const toggle = jest.fn(); - const popup = shallow( - <BubblePopupHelper - isOpen={false} - position="bottomright" - togglePopup={toggle} - popup={ - <BubblePopup> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper>, - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render with custom classes', () => { - const toggle = jest.fn(); - const popup = shallow( - <BubblePopupHelper - customClass="myhelperclass" - isOpen={true} - position="bottomright" - togglePopup={toggle} - popup={ - <BubblePopup customClass="mypopupclass"> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper>, - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render the popup with offset', () => { - const toggle = jest.fn(); - const popup = mount( - <BubblePopupHelper - isOpen={true} - offset={{ vertical: 5, horizontal: 2 }} - position="bottomright" - togglePopup={toggle} - popup={ - <BubblePopup> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper> - ); - expect(popup.find('BubblePopup')).toMatchSnapshot(); -}); - -it('should render an open popup on the left', () => { - const toggle = jest.fn(); - const popup = mount( - <BubblePopupHelper - isOpen={true} - offset={{ vertical: 0, horizontal: 2 }} - position="bottomleft" - togglePopup={toggle} - popup={ - <BubblePopup> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper> - ); - expect(popup.find('BubblePopup')).toMatchSnapshot(); -}); - -it('should correctly handle clicks on the button', () => { - const toggle = jest.fn(() => popup.setProps({ isOpen: !popup.props().isOpen })); - const popup = shallow( - <BubblePopupHelper - isOpen={false} - offset={{ vertical: 0, horizontal: 2 }} - position="bottomleft" - togglePopup={toggle} - popup={ - <BubblePopup> - <span>test</span> - </BubblePopup> - }> - <button onClick={toggle}>open</button> - </BubblePopupHelper>, - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); - click(popup.find('button')); - expect(toggle.mock.calls.length).toBe(1); - expect(popup).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap deleted file mode 100644 index 08a7cb8825e..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render popup 1`] = ` -<div - className="bubble-popup custom" - style={ - Object { - "right": 0, - "top": 0, - } - } -> - <span> - test - </span> - <div - className="bubble-popup-arrow" - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap deleted file mode 100644 index 51f8e93fda5..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap +++ /dev/null @@ -1,182 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should correctly handle clicks on the button 1`] = ` -<div - className="bubble-popup-helper" - onClick={[Function]} - role="tooltip" - tabIndex={0} -> - <button - onClick={[MockFunction]} - > - open - </button> -</div> -`; - -exports[`should correctly handle clicks on the button 2`] = ` -<div - className="bubble-popup-helper" - onClick={[Function]} - role="tooltip" - tabIndex={0} -> - <button - onClick={ - [MockFunction] { - "calls": Array [ - Array [ - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, - ], - ], - } - } - > - open - </button> - <div> - <BubblePopup - popupPosition={ - Object { - "right": 0, - "top": 0, - } - } - > - <span> - test - </span> - </BubblePopup> - </div> -</div> -`; - -exports[`should render an open popup on the left 1`] = ` -<BubblePopup - popupPosition={ - Object { - "left": 2, - "top": 0, - } - } -> - <div - className="bubble-popup" - style={Object {}} - > - <span> - test - </span> - <div - className="bubble-popup-arrow" - /> - </div> -</BubblePopup> -`; - -exports[`should render an open popup on the right 1`] = ` -<div - className="bubble-popup-helper" - onClick={[Function]} - role="tooltip" - tabIndex={0} -> - <button - onClick={[MockFunction]} - > - open - </button> - <div> - <BubblePopup - popupPosition={ - Object { - "right": 0, - "top": 0, - } - } - > - <span> - test - </span> - </BubblePopup> - </div> -</div> -`; - -exports[`should render the popup helper with a closed popup 1`] = ` -<div - className="bubble-popup-helper" - onClick={[Function]} - role="tooltip" - tabIndex={0} -> - <button - onClick={[MockFunction]} - > - open - </button> -</div> -`; - -exports[`should render the popup with offset 1`] = ` -<BubblePopup - popupPosition={ - Object { - "right": 2, - "top": 5, - } - } -> - <div - className="bubble-popup" - style={Object {}} - > - <span> - test - </span> - <div - className="bubble-popup-arrow" - /> - </div> -</BubblePopup> -`; - -exports[`should render with custom classes 1`] = ` -<div - className="bubble-popup-helper" - onClick={[Function]} - role="tooltip" - tabIndex={0} -> - <button - onClick={[MockFunction]} - > - open - </button> - <div> - <BubblePopup - customClass="mypopupclass" - popupPosition={ - Object { - "right": 0, - "top": 0, - } - } - > - <span> - test - </span> - </BubblePopup> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx index f1173c88e75..252d26ef618 100644 --- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -28,34 +28,24 @@ import { Button } from '../ui/buttons'; interface Props { className?: string; children: React.ReactNode; - menuClassName?: string; - menuPosition?: 'left' | 'right'; onOpen?: () => void; small?: boolean; toggleClassName?: string; } -export default function ActionsDropdown({ menuPosition = 'right', ...props }: Props) { +export default function ActionsDropdown(props: Props) { return ( - <Dropdown onOpen={props.onOpen}> - {({ onToggleClick, open }) => ( - <div className={classNames('dropdown', props.className, { open })}> - <Button - className={classNames('dropdown-toggle', props.toggleClassName, { - 'button-small': props.small - })} - onClick={onToggleClick}> - <SettingsIcon className="text-text-bottom" /> - <i className="icon-dropdown little-spacer-left" /> - </Button> - <ul - className={classNames('dropdown-menu', props.menuClassName, { - 'dropdown-menu-right': menuPosition === 'right' - })}> - {props.children} - </ul> - </div> - )} + <Dropdown + className={props.className} + onOpen={props.onOpen} + overlay={<ul className="menu">{props.children}</ul>}> + <Button + className={classNames('dropdown-toggle', props.toggleClassName, { + 'button-small': props.small + })}> + <SettingsIcon className="text-text-bottom" /> + <i className="icon-dropdown little-spacer-left" /> + </Button> </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index e5794463a32..886a43211e4 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -151,73 +151,71 @@ export default class DateInput extends React.PureComponent<Props, State> { return ( <OutsideClickHandler onClickOutside={this.closeCalendar}> - {({ ref }) => ( - <span className={classNames('date-input-control', this.props.className)} ref={ref}> - <input - className={classNames('date-input-control-input', this.props.inputClassName, { - 'is-filled': this.props.value !== undefined - })} - name={this.props.name} - onFocus={this.openCalendar} - placeholder={this.props.placeholder} - readOnly={true} - ref={node => (this.input = node)} - type="text" - value={formattedValue || ''} - /> - <CalendarIcon className="date-input-control-icon" fill="" /> - {this.props.value !== undefined && ( - <ButtonIcon - className="button-tiny date-input-control-reset" - color={theme.gray60} - onClick={this.handleResetClick}> - <ClearIcon size={12} /> - </ButtonIcon> - )} - {this.state.open && ( - <div className="date-input-calendar"> - <nav className="date-input-calendar-nav"> - <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}> - <ChevronLeftIcon /> - </ButtonIcon> - <div className="date-input-calender-month"> - <Select - className="date-input-calender-month-select" - onChange={this.handleCurrentMonthChange} - options={months.map(month => ({ - label: getShortMonthName(month), - value: month - }))} - value={this.state.currentMonth.getMonth()} - /> - <Select - className="date-input-calender-month-select spacer-left" - onChange={this.handleCurrentYearChange} - options={years.map(year => ({ label: String(year), value: year }))} - value={this.state.currentMonth.getFullYear()} - /> - </div> - <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}> - <ChevronRightIcon /> - </ButtonIcon> - </nav> - <DayPicker - captionElement={<NullComponent />} - disabledDays={{ after, before: minDate }} - firstDayOfWeek={1} - modifiers={modifiers} - month={this.state.currentMonth} - navbarElement={<NullComponent />} - onDayClick={this.handleDayClick} - onDayMouseEnter={this.handleDayMouseEnter} - selectedDays={selectedDays} - weekdaysLong={weekdaysLong} - weekdaysShort={weekdaysShort} - /> - </div> - )} - </span> - )} + <span className={classNames('date-input-control', this.props.className)}> + <input + className={classNames('date-input-control-input', this.props.inputClassName, { + 'is-filled': this.props.value !== undefined + })} + name={this.props.name} + onFocus={this.openCalendar} + placeholder={this.props.placeholder} + readOnly={true} + ref={node => (this.input = node)} + type="text" + value={formattedValue || ''} + /> + <CalendarIcon className="date-input-control-icon" fill="" /> + {this.props.value !== undefined && ( + <ButtonIcon + className="button-tiny date-input-control-reset" + color={theme.gray60} + onClick={this.handleResetClick}> + <ClearIcon size={12} /> + </ButtonIcon> + )} + {this.state.open && ( + <div className="date-input-calendar"> + <nav className="date-input-calendar-nav"> + <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}> + <ChevronLeftIcon /> + </ButtonIcon> + <div className="date-input-calender-month"> + <Select + className="date-input-calender-month-select" + onChange={this.handleCurrentMonthChange} + options={months.map(month => ({ + label: getShortMonthName(month), + value: month + }))} + value={this.state.currentMonth.getMonth()} + /> + <Select + className="date-input-calender-month-select spacer-left" + onChange={this.handleCurrentYearChange} + options={years.map(year => ({ label: String(year), value: year }))} + value={this.state.currentMonth.getFullYear()} + /> + </div> + <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}> + <ChevronRightIcon /> + </ButtonIcon> + </nav> + <DayPicker + captionElement={<NullComponent />} + disabledDays={{ after, before: minDate }} + firstDayOfWeek={1} + modifiers={modifiers} + month={this.state.currentMonth} + navbarElement={<NullComponent />} + onDayClick={this.handleDayClick} + onDayMouseEnter={this.handleDayMouseEnter} + selectedDays={selectedDays} + weekdaysLong={weekdaysLong} + weekdaysShort={weekdaysShort} + /> + </div> + )} + </span> </OutsideClickHandler> ); } diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx index e23a99b7e25..9c13423e454 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx +++ b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx @@ -18,32 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; - -export interface BubblePopupPosition { - top?: number; - left?: number; - right?: number; -} interface Props { - customClass?: string; children: React.ReactNode; - position?: BubblePopupPosition; + onClick: () => void; } -/** - * Deprecated. - * Use <Popup /> instead. - */ -export default function BubblePopup(props: Props) { - const popupClass = classNames('bubble-popup', props.customClass); - const popupStyle = { ...props.position }; +export default class DocumentClickHandler extends React.Component<Props> { + componentDidMount() { + setTimeout(() => { + this.addClickHandler(); + }, 0); + } + + componentWillUnmount() { + this.removeClickHandler(); + } + + addClickHandler = () => { + document.addEventListener('click', this.handleDocumentClick); + }; + + removeClickHandler = () => { + document.removeEventListener('click', this.handleDocumentClick); + }; + + handleDocumentClick = () => { + this.props.onClick(); + }; - return ( - <div className={popupClass} style={popupStyle}> - {props.children} - <div className="bubble-popup-arrow" /> - </div> - ); + render() { + return this.props.children; + } } diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx index 3b82fd1c408..a90414a235c 100644 --- a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx @@ -18,16 +18,33 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; +import ScreenPositionFixer from './ScreenPositionFixer'; +import Toggler from './Toggler'; +import { Popup, PopupPlacement } from '../ui/popups'; + +interface OnClickCallback { + (event?: React.SyntheticEvent<HTMLElement>): void; +} interface RenderProps { closeDropdown: () => void; - onToggleClick: (event?: React.SyntheticEvent<HTMLElement>) => void; + onToggleClick: OnClickCallback; open: boolean; } interface Props { - children: (renderProps: RenderProps) => JSX.Element; + children: + | ((renderProps: RenderProps) => JSX.Element) + | React.ReactElement<{ onClick: OnClickCallback }>; + className?: string; + closeOnClick?: boolean; + closeOnClickOutside?: boolean; onOpen?: () => void; + overlay: React.ReactNode; + overlayPlacement?: PopupPlacement; + noOverlayPadding?: boolean; + tagName?: string; } interface State { @@ -35,49 +52,18 @@ interface State { } export default class Dropdown extends React.PureComponent<Props, State> { - toggleNode?: HTMLElement; - - constructor(props: Props) { - super(props); - this.state = { open: false }; - } + state: State = { open: false }; componentDidUpdate(_: Props, prevState: State) { - if (!prevState.open && this.state.open) { - this.addClickHandler(); - if (this.props.onOpen) { - this.props.onOpen(); - } - } - - if (prevState.open && !this.state.open) { - this.removeClickHandler(); + if (!prevState.open && this.state.open && this.props.onOpen) { + this.props.onOpen(); } } - componentWillUnmount() { - this.removeClickHandler(); - } - - addClickHandler = () => { - window.addEventListener('click', this.handleWindowClick); - }; - - removeClickHandler = () => { - window.removeEventListener('click', this.handleWindowClick); - }; - - handleWindowClick = (event: MouseEvent) => { - if (!this.toggleNode || !this.toggleNode.contains(event.target as Node)) { - this.closeDropdown(); - } - }; - closeDropdown = () => this.setState({ open: false }); handleToggleClick = (event?: React.SyntheticEvent<HTMLElement>) => { if (event) { - this.toggleNode = event.currentTarget; event.preventDefault(); event.currentTarget.blur(); } @@ -85,10 +71,89 @@ export default class Dropdown extends React.PureComponent<Props, State> { }; render() { - return this.props.children({ - closeDropdown: this.closeDropdown, - onToggleClick: this.handleToggleClick, - open: this.state.open - }); + const a11yAttrs = { + 'aria-expanded': String(this.state.open), + 'aria-haspopup': 'true' + }; + + const child = React.isValidElement(this.props.children) + ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs }) + : this.props.children({ + closeDropdown: this.closeDropdown, + onToggleClick: this.handleToggleClick, + open: this.state.open + }); + + const { closeOnClick = true, closeOnClickOutside = false } = this.props; + + const toggler = ( + <Toggler + closeOnClick={closeOnClick} + closeOnClickOutside={closeOnClickOutside} + onRequestClose={this.closeDropdown} + open={this.state.open} + overlay={ + <DropdownOverlay + noPadding={this.props.noOverlayPadding} + placement={this.props.overlayPlacement}> + {this.props.overlay} + </DropdownOverlay> + }> + {child} + </Toggler> + ); + + return React.createElement( + this.props.tagName || 'div', + { className: classNames('dropdown', this.props.className) }, + toggler + ); + } +} + +interface OverlayProps { + className?: string; + children: React.ReactNode; + noPadding?: boolean; + placement?: PopupPlacement; +} + +// TODO use the same styling for <Select /> +// TODO use the same styling for <DateInput /> + +export class DropdownOverlay extends React.Component<OverlayProps> { + get placement() { + return this.props.placement || PopupPlacement.Bottom; + } + + renderPopup = (leftFix?: number, topFix?: number) => ( + <Popup + arrowStyle={ + leftFix !== undefined && topFix !== undefined + ? { transform: `translate(${-leftFix}px, ${-topFix}px)` } + : undefined + } + className={this.props.className} + noPadding={this.props.noPadding} + placement={this.placement} + style={ + leftFix !== undefined && topFix !== undefined + ? { marginLeft: `calc(50% + ${leftFix}px)` } + : undefined + }> + {this.props.children} + </Popup> + ); + + render() { + if (this.placement === PopupPlacement.Bottom) { + return ( + <ScreenPositionFixer> + {({ leftFix, topFix }) => this.renderPopup(leftFix, topFix)} + </ScreenPositionFixer> + ); + } else { + return this.renderPopup(); + } } } diff --git a/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx b/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx index 3886a89e19f..f5e5d6ec587 100644 --- a/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx +++ b/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx @@ -18,9 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { findDOMNode } from 'react-dom'; interface Props { - children: (props: { ref: React.Ref<Element> }) => React.ReactNode; + children: React.ReactNode; onClickOutside: () => void; } @@ -28,7 +29,9 @@ export default class OutsideClickHandler extends React.Component<Props> { element?: Element | null; componentDidMount() { - this.addClickHandler(); + setTimeout(() => { + this.addClickHandler(); + }, 0); } componentWillUnmount() { @@ -44,16 +47,14 @@ export default class OutsideClickHandler extends React.Component<Props> { }; handleWindowClick = (event: MouseEvent) => { - if (!this.element || !this.element.contains(event.target as Node)) { + // eslint-disable-next-line react/no-find-dom-node + const node = findDOMNode(this); + if (!node || !node.contains(event.target as Node)) { this.props.onClickOutside(); } }; - handleRef = (element: Element | null) => { - this.element = element; - }; - render() { - return this.props.children({ ref: this.handleRef }); + return this.props.children; } } diff --git a/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx b/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx new file mode 100644 index 00000000000..ebf28cf924a --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx @@ -0,0 +1,108 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { findDOMNode } from 'react-dom'; +import { throttle } from 'lodash'; +import { grid } from '../../app/theme'; + +const EDGE_MARGIN = 0.5 * grid; + +interface Props { + /** + * First time `children` are rendered with `undefined` fixes to measure the offset. + * Second time it renders with the computed fixes. + */ + children: (props: Fixes) => React.ReactNode; + + /** + * Use this flag to force re-positioning. + * Use cases: + * - when you need to measure `children` size first + * - when you load content asynchronously + */ + ready?: boolean; +} + +interface Fixes { + leftFix?: number; + topFix?: number; +} + +export default class ScreenPositionFixer extends React.Component<Props, Fixes> { + throttledPosition: () => void; + + constructor(props: Props) { + super(props); + this.state = {}; + this.throttledPosition = throttle(this.position, 50); + } + + componentDidMount() { + this.addEventListeners(); + this.position(); + } + + componentDidUpdate(prevProps: Props) { + if (!prevProps.ready && this.props.ready) { + this.position(); + } + } + + componentWillUnmount() { + this.removeEventListeners(); + } + + addEventListeners = () => { + window.addEventListener('resize', this.throttledPosition); + }; + + removeEventListeners = () => { + window.removeEventListener('resize', this.throttledPosition); + }; + + position = () => { + // eslint-disable-next-line react/no-find-dom-node + const node = findDOMNode(this); + + const { width, height, left, top } = node.getBoundingClientRect(); + + const { clientHeight, clientWidth } = document.body; + + let leftFix = 0; + if (left < EDGE_MARGIN) { + leftFix = EDGE_MARGIN - left; + } else if (left + width > clientWidth - EDGE_MARGIN) { + leftFix = clientWidth - EDGE_MARGIN - left - width; + } + + let topFix = 0; + if (top < EDGE_MARGIN) { + topFix = EDGE_MARGIN - top; + } else if (top + height > clientHeight - EDGE_MARGIN) { + topFix = clientHeight - EDGE_MARGIN - top - height; + } + + this.setState({ leftFix, topFix }); + }; + + render() { + return this.props.children(this.state); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/Toggler.tsx b/server/sonar-web/src/main/js/components/controls/Toggler.tsx new file mode 100644 index 00000000000..37bb43bf597 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Toggler.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DocumentClickHandler from './DocumentClickHandler'; +import OutsideClickHandler from './OutsideClickHandler'; + +export interface Props { + children?: React.ReactNode; + closeOnClick?: boolean; + closeOnClickOutside?: boolean; + closeOnEscape?: boolean; + onRequestClose: () => void; + open: boolean; + overlay: React.ReactNode; +} + +export default class Toggler extends React.Component<Props> { + componentDidMount() { + if (this.props.open && isTrueOrUndefined(this.props.closeOnEscape)) { + this.addEventListeners(); + } + } + + componentDidUpdate(prevProps: Props) { + if (!prevProps.open && this.props.open && isTrueOrUndefined(this.props.closeOnEscape)) { + this.addEventListeners(); + } else if (prevProps.open && !this.props.open) { + this.removeEventListeners(); + } else if ( + isTrueOrUndefined(prevProps.closeOnEscape) && + !isTrueOrUndefined(this.props.closeOnEscape) + ) { + this.removeEventListeners(); + } + } + + componentWillUnmount() { + this.removeEventListeners(); + } + + addEventListeners() { + document.addEventListener('keydown', this.handleKeyDown, false); + } + + removeEventListeners() { + document.removeEventListener('keydown', this.handleKeyDown, false); + } + + handleKeyDown = (event: KeyboardEvent) => { + // Escape key + if (event.keyCode === 27) { + this.props.onRequestClose(); + } + }; + + renderOverlay() { + const { + closeOnClick = false, + closeOnClickOutside = true, + onRequestClose, + overlay + } = this.props; + + if (closeOnClick) { + return <DocumentClickHandler onClick={onRequestClose}>{overlay}</DocumentClickHandler>; + } else if (closeOnClickOutside) { + return <OutsideClickHandler onClickOutside={onRequestClose}>{overlay}</OutsideClickHandler>; + } else { + return overlay; + } + } + + render() { + return ( + <> + {this.props.children} + {this.props.open && this.renderOverlay()} + </> + ); + } +} + +function isTrueOrUndefined(x: boolean | undefined) { + return x === true || x === undefined; +} diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index ca6803887e9..14dfa11da66 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { createPortal, findDOMNode } from 'react-dom'; import { throttle } from 'lodash'; +import ScreePositionFixer from './ScreenPositionFixer'; import './Tooltip.css'; export type Placement = 'bottom' | 'right' | 'left' | 'top'; @@ -39,9 +40,7 @@ interface Props { interface Measurements { height: number; left: number; - leftFix: number; top: number; - topFix: number; width: number; } @@ -55,8 +54,6 @@ function isMeasured(state: State): state is OwnState & Measurements { return state.height !== undefined; } -const EDGE_MARGIN = 4; - export default function Tooltip(props: Props) { // allows to pass `undefined` to `overlay` to avoid rendering a tooltip // can useful in some cases to render the tooltip conditionally @@ -182,31 +179,11 @@ export class TooltipInner extends React.Component<Props, State> { break; } - // make sure the tooltip fits in the document - // it may go out of the current viewport, if it has scrolls - const { scrollWidth, scrollHeight } = document.documentElement; - - let leftFix = 0; - if (left < EDGE_MARGIN) { - leftFix = EDGE_MARGIN - left; - } else if (left + width > scrollWidth - EDGE_MARGIN) { - leftFix = scrollWidth - EDGE_MARGIN - left - width; - } - - let topFix = 0; - if (top < EDGE_MARGIN) { - topFix = EDGE_MARGIN - top; - } else if (top + height > scrollHeight - EDGE_MARGIN) { - topFix = scrollHeight - EDGE_MARGIN - top - height; - } - // save width and height (and later set in `render`) to avoid resizing the tooltip element, // when it's placed close to the window edge const measurements: Measurements = { left: window.pageXOffset + left, - leftFix, top: window.pageYOffset + top, - topFix, width, height }; @@ -217,9 +194,7 @@ export class TooltipInner extends React.Component<Props, State> { clearPosition = () => { this.setState({ left: undefined, - leftFix: undefined, top: undefined, - topFix: undefined, width: undefined, height: undefined }); @@ -280,31 +255,35 @@ export class TooltipInner extends React.Component<Props, State> { })} {this.isVisible() && ( <TooltipPortal> - <div - className={`${classNameSpace} ${this.getPlacement()}`} - onMouseEnter={this.handleOverlayMouseEnter} - onMouseLeave={this.handleOverlayMouseLeave} - ref={this.tooltipNodeRef} - style={ - isMeasured(this.state) - ? { - left: this.state.left + this.state.leftFix, - top: this.state.top + this.state.topFix, - width: this.state.width, - height: this.state.height + <ScreePositionFixer ready={isMeasured(this.state)}> + {({ leftFix = 0, topFix = 0 }) => ( + <div + className={`${classNameSpace} ${this.getPlacement()}`} + onMouseEnter={this.handleOverlayMouseEnter} + onMouseLeave={this.handleOverlayMouseLeave} + ref={this.tooltipNodeRef} + style={ + isMeasured(this.state) + ? { + left: this.state.left + leftFix, + top: this.state.top + topFix, + width: this.state.width, + height: this.state.height + } + : undefined + }> + <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div> + <div + className={`${classNameSpace}-arrow`} + style={ + isMeasured(this.state) + ? { marginLeft: -leftFix, marginTop: -topFix } + : undefined } - : undefined - }> - <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div> - <div - className={`${classNameSpace}-arrow`} - style={ - isMeasured(this.state) - ? { marginLeft: -this.state.leftFix, marginTop: -this.state.topFix } - : undefined - } - /> - </div> + /> + </div> + )} + </ScreePositionFixer> </TooltipPortal> )} </> diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx index 37c974480ec..564f884d920 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx @@ -39,13 +39,12 @@ it('should render', () => { const { wrapper } = shallowRender(); expect(wrapper).toMatchSnapshot(); - expect(wrapper.dive()).toMatchSnapshot(); wrapper.setProps({ value: dateA }); - expect(wrapper.dive()).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); wrapper.setState({ open: true }); - expect(wrapper.dive()).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); it('should change current month', () => { @@ -76,8 +75,9 @@ it('should select a day', () => { instance.handleDayClick(dateA, { outside: undefined, today: undefined }); expect(onChange).lastCalledWith(dateA); + wrapper.update(); expect(wrapper.state().open).toBe(false); - expect(wrapper.dive()).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); instance.handleResetClick(); expect(onChange).lastCalledWith(undefined); @@ -90,7 +90,7 @@ it('should hightlightFrom range', () => { const dateC = addDays(dateA, 3); instance.handleDayMouseEnter(dateC, { outside: undefined, today: undefined }); wrapper.update(); - const dayPicker = wrapper.dive().find('DayPicker'); + const dayPicker = wrapper.find('DayPicker'); expect(dayPicker.prop('modifiers')).toEqual({ highlighted: { from: dateA, to: dateC } }); expect(dayPicker.prop('selectedDays')).toEqual([dateA]); }); @@ -102,7 +102,7 @@ it('should hightlightTo range', () => { const dateC = subDays(dateB, 5); instance.handleDayMouseEnter(dateC, { outside: undefined, today: undefined }); wrapper.update(); - const dayPicker = wrapper.dive().find('DayPicker'); + const dayPicker = wrapper.find('DayPicker'); expect(dayPicker.prop('modifiers')).toEqual({ highlighted: { from: dateC, to: dateB } }); expect(dayPicker.prop('selectedDays')).toEqual([dateB]); }); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx index 096dc8356ba..52df4675a0b 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx @@ -18,28 +18,92 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { shallow } from 'enzyme'; -import Dropdown from '../Dropdown'; +import { shallow, mount, ShallowWrapper } from 'enzyme'; +import Dropdown, { DropdownOverlay } from '../Dropdown'; import { Button } from '../../ui/buttons'; import { click } from '../../../helpers/testUtils'; +import { PopupPlacement } from '../../ui/popups'; -it('renders', () => { - expect( - shallow(<Dropdown>{() => <div />}</Dropdown>) - .find('div') - .exists() - ).toBeTruthy(); -}); +describe('Dropdown', () => { + it('renders', () => { + expect( + shallow(<Dropdown overlay={<div id="overlay" />}>{() => <div />}</Dropdown>) + .find('div') + .exists() + ).toBeTruthy(); + }); + + it('toggles with element child', () => { + checkToggle( + shallow( + <Dropdown overlay={<div id="overlay" />}> + <Button /> + </Dropdown> + ) + ); + + checkToggle( + shallow( + <Dropdown overlay={<div id="overlay" />}> + <a href="#">click me!</a> + </Dropdown> + ), + 'a' + ); + }); + + it('toggles with render prop', () => { + checkToggle( + shallow( + <Dropdown overlay={<div id="overlay" />}> + {({ onToggleClick }) => <Button onClick={onToggleClick} />} + </Dropdown> + ) + ); + }); -it('toggles', () => { - const wrapper = shallow( - <Dropdown>{({ onToggleClick }) => <Button onClick={onToggleClick} />}</Dropdown> - ); - expect(wrapper.state()).toEqual({ open: false }); + it('should call onOpen', () => { + const onOpen = jest.fn(); + const wrapper = mount( + <Dropdown onOpen={onOpen} overlay={<div id="overlay" />}> + <Button /> + </Dropdown> + ); + expect(onOpen).not.toBeCalled(); + click(wrapper.find('Button')); + expect(onOpen).toBeCalled(); + }); + + function checkToggle(wrapper: ShallowWrapper, selector = 'Button') { + expect(wrapper.state()).toEqual({ open: false }); + + click(wrapper.find(selector)); + expect(wrapper.state()).toEqual({ open: true }); + + click(wrapper.find(selector)); + expect(wrapper.state()).toEqual({ open: false }); + } +}); - click(wrapper.find('Button')); - expect(wrapper.state()).toEqual({ open: true }); +describe('DropdownOverlay', () => { + it('should render overlay with screen fixer', () => { + const wrapper = shallow( + <DropdownOverlay> + <div /> + </DropdownOverlay>, + // disable ScreenPositionFixer positioning + { disableLifecycleMethods: true } + ); + expect(wrapper.is('ScreenPositionFixer')).toBe(true); + expect(wrapper.dive().is('Popup')).toBe(true); + }); - click(wrapper.find('Button')); - expect(wrapper.state()).toEqual({ open: false }); + it('should render overlay without screen fixer', () => { + const wrapper = shallow( + <DropdownOverlay placement={PopupPlacement.BottomRight}> + <div /> + </DropdownOverlay> + ); + expect(wrapper.is('Popup')).toBe(true); + }); }); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx new file mode 100644 index 00000000000..9e389326629 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { mount } from 'enzyme'; +import ScreenPositionFixer from '../ScreenPositionFixer'; +import { resizeWindowTo } from '../../../helpers/testUtils'; + +jest.mock('lodash', () => { + const lodash = require.requireActual('lodash'); + lodash.throttle = (fn: any) => () => fn(); + return lodash; +}); + +jest.mock('react-dom', () => ({ + findDOMNode: jest.fn(() => ({ + getBoundingClientRect: () => ({ width: 0, height: 0, left: 0, top: 0 }) + })) +})); + +beforeEach(() => { + setNodeRect({ width: 50, height: 50, left: 50, top: 50 }); + resizeWindowTo(1000, 1000); +}); + +it('should fix position', () => { + const renderer = jest.fn(() => <div />); + mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>); + + setNodeRect({ width: 50, height: 50, left: 50, top: 50 }); + resizeWindowTo(75, 1000); + expect(renderer).toHaveBeenLastCalledWith({ leftFix: -29, topFix: 0 }); + + resizeWindowTo(1000, 75); + expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: -29 }); + + setNodeRect({ width: 50, height: 50, left: -10, top: 50 }); + resizeWindowTo(1000, 1000); + expect(renderer).toHaveBeenLastCalledWith({ leftFix: 14, topFix: 0 }); + + setNodeRect({ width: 50, height: 50, left: 50, top: -10 }); + resizeWindowTo(); + expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 14 }); +}); + +it('should render two times', () => { + const renderer = jest.fn(() => <div />); + mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>); + expect(renderer).toHaveBeenCalledTimes(2); + expect(renderer).toHaveBeenCalledWith({}); + expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 0 }); +}); + +it('should re-position when `ready` turns to `true`', () => { + const renderer = jest.fn(() => <div />); + const wrapper = mount(<ScreenPositionFixer ready={false}>{renderer}</ScreenPositionFixer>); + expect(renderer).toHaveBeenCalledTimes(2); + wrapper.setProps({ ready: true }); + // 2 + 1 (props change) + 1 (new measurement) + expect(renderer).toHaveBeenCalledTimes(4); +}); + +it('should re-position when window is resized', () => { + const renderer = jest.fn(() => <div />); + const wrapper = mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>); + expect(renderer).toHaveBeenCalledTimes(2); + + resizeWindowTo(); + // 2 + 1 (new measurement) + expect(renderer).toHaveBeenCalledTimes(3); + + wrapper.unmount(); + resizeWindowTo(); + expect(renderer).toHaveBeenCalledTimes(3); +}); + +function setNodeRect(rect: { width: number; height: number; left: number; top: number }) { + const findDOMNode = require('react-dom').findDOMNode as jest.Mock<any>; + findDOMNode.mockImplementation(() => ({ + getBoundingClientRect: () => rect + })); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx new file mode 100644 index 00000000000..ee087ecbc6c --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Toggler, { Props } from '../Toggler'; +import { keydown } from '../../../helpers/testUtils'; + +it('should render only children', () => { + expect(shallowRender({ open: false })).toMatchSnapshot(); +}); + +it('should render children and overlay', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should render when closeOnClick=true', () => { + expect(shallowRender({ closeOnClick: true })).toMatchSnapshot(); +}); + +it('should not render click wrappers', () => { + expect(shallowRender({ closeOnClick: false, closeOnClickOutside: false })).toMatchSnapshot(); +}); + +it('should close on escape', () => { + const onRequestClose = jest.fn(); + const wrapper = shallowRender({ + closeOnClick: false, + closeOnClickOutside: false, + onRequestClose + }); + keydown(27); + expect(onRequestClose).toHaveBeenCalledTimes(1); + + wrapper.setProps({ closeOnEscape: false }); + keydown(27); + expect(onRequestClose).toHaveBeenCalledTimes(1); + + wrapper.setProps({ open: false }); + wrapper.setProps({ closeOnEscape: true, open: true }); + keydown(27); + expect(onRequestClose).toHaveBeenCalledTimes(2); + + wrapper.unmount(); + keydown(27); + expect(onRequestClose).toHaveBeenCalledTimes(2); +}); + +function shallowRender(props?: Partial<Props>) { + return shallow( + <Toggler onRequestClose={jest.fn()} open={true} overlay={<div id="overlay" />} {...props}> + <div id="toggle" /> + </Toggler> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap index af6836077c7..5485b05984e 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap @@ -3,274 +3,284 @@ exports[`should render 1`] = ` <OutsideClickHandler onClickOutside={[Function]} -/> -`; - -exports[`should render 2`] = ` -<span - className="date-input-control" > - <input - className="date-input-control-input" - onFocus={[Function]} - placeholder="placeholder" - readOnly={true} - type="text" - value="" - /> - <CalendarIcon - className="date-input-control-icon" - fill="" - /> -</span> + <span + className="date-input-control" + > + <input + className="date-input-control-input" + onFocus={[Function]} + placeholder="placeholder" + readOnly={true} + type="text" + value="" + /> + <CalendarIcon + className="date-input-control-icon" + fill="" + /> + </span> +</OutsideClickHandler> `; -exports[`should render 3`] = ` -<span - className="date-input-control" +exports[`should render 2`] = ` +<OutsideClickHandler + onClickOutside={[Function]} > - <input - className="date-input-control-input is-filled" - onFocus={[Function]} - placeholder="placeholder" - readOnly={true} - type="text" - value="Jan 17, 2018" - /> - <CalendarIcon - className="date-input-control-icon" - fill="" - /> - <ButtonIcon - className="button-tiny date-input-control-reset" - color="#999" - onClick={[Function]} + <span + className="date-input-control" > - <ClearIcon - size={12} + <input + className="date-input-control-input is-filled" + onFocus={[Function]} + placeholder="placeholder" + readOnly={true} + type="text" + value="Jan 17, 2018" /> - </ButtonIcon> -</span> + <CalendarIcon + className="date-input-control-icon" + fill="" + /> + <ButtonIcon + className="button-tiny date-input-control-reset" + color="#999" + onClick={[Function]} + > + <ClearIcon + size={12} + /> + </ButtonIcon> + </span> +</OutsideClickHandler> `; -exports[`should render 4`] = ` -<span - className="date-input-control" +exports[`should render 3`] = ` +<OutsideClickHandler + onClickOutside={[Function]} > - <input - className="date-input-control-input is-filled" - onFocus={[Function]} - placeholder="placeholder" - readOnly={true} - type="text" - value="Jan 17, 2018" - /> - <CalendarIcon - className="date-input-control-icon" - fill="" - /> - <ButtonIcon - className="button-tiny date-input-control-reset" - color="#999" - onClick={[Function]} + <span + className="date-input-control" > - <ClearIcon - size={12} + <input + className="date-input-control-input is-filled" + onFocus={[Function]} + placeholder="placeholder" + readOnly={true} + type="text" + value="Jan 17, 2018" /> - </ButtonIcon> - <div - className="date-input-calendar" - > - <nav - className="date-input-calendar-nav" + <CalendarIcon + className="date-input-control-icon" + fill="" + /> + <ButtonIcon + className="button-tiny date-input-control-reset" + color="#999" + onClick={[Function]} > - <ButtonIcon - className="button-small" - onClick={[Function]} - > - <ChevronLeftIcon /> - </ButtonIcon> - <div - className="date-input-calender-month" + <ClearIcon + size={12} + /> + </ButtonIcon> + <div + className="date-input-calendar" + > + <nav + className="date-input-calendar-nav" > - <Select - className="date-input-calender-month-select" - onChange={[Function]} - options={ - Array [ - Object { - "label": "Jan", - "value": 0, - }, - Object { - "label": "Feb", - "value": 1, - }, - Object { - "label": "Mar", - "value": 2, - }, - Object { - "label": "Apr", - "value": 3, - }, - Object { - "label": "May", - "value": 4, - }, - Object { - "label": "Jun", - "value": 5, - }, - Object { - "label": "Jul", - "value": 6, - }, - Object { - "label": "Aug", - "value": 7, - }, - Object { - "label": "Sep", - "value": 8, - }, - Object { - "label": "Oct", - "value": 9, - }, - Object { - "label": "Nov", - "value": 10, - }, - Object { - "label": "Dec", - "value": 11, - }, - ] + <ButtonIcon + className="button-small" + onClick={[Function]} + > + <ChevronLeftIcon /> + </ButtonIcon> + <div + className="date-input-calender-month" + > + <Select + className="date-input-calender-month-select" + onChange={[Function]} + options={ + Array [ + Object { + "label": "Jan", + "value": 0, + }, + Object { + "label": "Feb", + "value": 1, + }, + Object { + "label": "Mar", + "value": 2, + }, + Object { + "label": "Apr", + "value": 3, + }, + Object { + "label": "May", + "value": 4, + }, + Object { + "label": "Jun", + "value": 5, + }, + Object { + "label": "Jul", + "value": 6, + }, + Object { + "label": "Aug", + "value": 7, + }, + Object { + "label": "Sep", + "value": 8, + }, + Object { + "label": "Oct", + "value": 9, + }, + Object { + "label": "Nov", + "value": 10, + }, + Object { + "label": "Dec", + "value": 11, + }, + ] + } + value={0} + /> + <Select + className="date-input-calender-month-select spacer-left" + onChange={[Function]} + options={ + Array [ + Object { + "label": "2008", + "value": 2008, + }, + Object { + "label": "2009", + "value": 2009, + }, + Object { + "label": "2010", + "value": 2010, + }, + Object { + "label": "2011", + "value": 2011, + }, + Object { + "label": "2012", + "value": 2012, + }, + Object { + "label": "2013", + "value": 2013, + }, + Object { + "label": "2014", + "value": 2014, + }, + Object { + "label": "2015", + "value": 2015, + }, + Object { + "label": "2016", + "value": 2016, + }, + Object { + "label": "2017", + "value": 2017, + }, + Object { + "label": "2018", + "value": 2018, + }, + ] + } + value={2018} + /> + </div> + <ButtonIcon + className="button-small" + onClick={[Function]} + > + <ChevronRightIcon /> + </ButtonIcon> + </nav> + <DayPicker + captionElement={<NullComponent />} + disabledDays={ + Object { + "after": 2018-02-05T00:00:00.000Z, + "before": 2018-01-17T00:00:00.000Z, } - value={0} - /> - <Select - className="date-input-calender-month-select spacer-left" - onChange={[Function]} - options={ - Array [ - Object { - "label": "2008", - "value": 2008, - }, - Object { - "label": "2009", - "value": 2009, - }, - Object { - "label": "2010", - "value": 2010, - }, - Object { - "label": "2011", - "value": 2011, - }, - Object { - "label": "2012", - "value": 2012, - }, - Object { - "label": "2013", - "value": 2013, - }, - Object { - "label": "2014", - "value": 2014, - }, - Object { - "label": "2015", - "value": 2015, - }, - Object { - "label": "2016", - "value": 2016, - }, - Object { - "label": "2017", - "value": 2017, - }, - Object { - "label": "2018", - "value": 2018, - }, - ] - } - value={2018} - /> - </div> - <ButtonIcon - className="button-small" - onClick={[Function]} - > - <ChevronRightIcon /> - </ButtonIcon> - </nav> - <DayPicker - captionElement={<NullComponent />} - disabledDays={ - Object { - "after": 2018-02-05T00:00:00.000Z, - "before": 2018-01-17T00:00:00.000Z, } - } - firstDayOfWeek={1} - month={2018-01-17T00:00:00.000Z} - navbarElement={<NullComponent />} - onDayClick={[Function]} - onDayMouseEnter={[Function]} - selectedDays={ - Array [ - 2018-01-17T00:00:00.000Z, - ] - } - weekdaysLong={ - Array [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ] - } - weekdaysShort={ - Array [ - "Sun", - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - ] - } - /> - </div> -</span> + firstDayOfWeek={1} + month={2018-01-17T00:00:00.000Z} + navbarElement={<NullComponent />} + onDayClick={[Function]} + onDayMouseEnter={[Function]} + selectedDays={ + Array [ + 2018-01-17T00:00:00.000Z, + ] + } + weekdaysLong={ + Array [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ] + } + weekdaysShort={ + Array [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + ] + } + /> + </div> + </span> +</OutsideClickHandler> `; exports[`should select a day 1`] = ` -<span - className="date-input-control" +<OutsideClickHandler + onClickOutside={[Function]} > - <input - className="date-input-control-input" - onFocus={[Function]} - placeholder="placeholder" - readOnly={true} - type="text" - value="" - /> - <CalendarIcon - className="date-input-control-icon" - fill="" - /> -</span> + <span + className="date-input-control" + > + <input + className="date-input-control-input" + onFocus={[Function]} + placeholder="placeholder" + readOnly={true} + type="text" + value="" + /> + <CalendarIcon + className="date-input-control-icon" + fill="" + /> + </span> +</OutsideClickHandler> `; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap new file mode 100644 index 00000000000..a782ff8e6f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render click wrappers 1`] = ` +<React.Fragment> + <div + id="toggle" + /> + <div + id="overlay" + /> +</React.Fragment> +`; + +exports[`should render children and overlay 1`] = ` +<React.Fragment> + <div + id="toggle" + /> + <OutsideClickHandler + onClickOutside={[MockFunction]} + > + <div + id="overlay" + /> + </OutsideClickHandler> +</React.Fragment> +`; + +exports[`should render only children 1`] = ` +<React.Fragment> + <div + id="toggle" + /> +</React.Fragment> +`; + +exports[`should render when closeOnClick=true 1`] = ` +<React.Fragment> + <div + id="toggle" + /> + <DocumentClickHandler + onClick={[MockFunction]} + > + <div + id="overlay" + /> + </DocumentClickHandler> +</React.Fragment> +`; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap index 4a369aceaa4..4127f6e586e 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap @@ -8,22 +8,9 @@ exports[`should open & close 1`] = ` onMouseLeave={[Function]} /> <TooltipPortal> - <div - className="tooltip bottom" - onMouseEnter={[Function]} - onMouseLeave={[Function]} - > - <div - className="tooltip-inner" - > - <span - id="overlay" - /> - </div> - <div - className="tooltip-arrow" - /> - </div> + <ScreenPositionFixer + ready={false} + /> </TooltipPortal> </React.Fragment> `; @@ -56,22 +43,9 @@ exports[`should render 2`] = ` onMouseLeave={[Function]} /> <TooltipPortal> - <div - className="tooltip bottom" - onMouseEnter={[Function]} - onMouseLeave={[Function]} - > - <div - className="tooltip-inner" - > - <span - id="overlay" - /> - </div> - <div - className="tooltip-arrow" - /> - </div> + <ScreenPositionFixer + ready={false} + /> </TooltipPortal> </React.Fragment> `; diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js index 5f814a5347e..0598c4f9f07 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.js +++ b/server/sonar-web/src/main/js/components/issue/IssueView.js @@ -51,15 +51,14 @@ export default class IssueView extends React.PureComponent { handleCheck = (event /*: Event */) => { event.preventDefault(); - event.stopPropagation(); if (this.props.onCheck) { this.props.onCheck(this.props.issue.key, event); } }; handleClick = (event /*: Event & { target: HTMLElement } */) => { - event.preventDefault(); - if (this.props.onClick) { + if (!isClickable(event.target) && this.props.onClick) { + event.preventDefault(); this.props.onClick(this.props.issue.key); } }; @@ -100,12 +99,12 @@ export default class IssueView extends React.PureComponent { togglePopup={this.props.togglePopup} /> <IssueActionsBar - issue={issue} currentPopup={this.props.currentPopup} + issue={issue} onAssign={this.props.onAssign} + onChange={this.props.onChange} onFail={this.props.onFail} togglePopup={this.props.togglePopup} - onChange={this.props.onChange} /> {issue.comments && issue.comments.length > 0 && ( @@ -114,8 +113,8 @@ export default class IssueView extends React.PureComponent { <IssueCommentLine comment={comment} key={comment.key} - onEdit={this.editComment} onDelete={this.deleteComment} + onEdit={this.editComment} /> ))} </div> @@ -137,3 +136,12 @@ export default class IssueView extends React.PureComponent { ); } } + +function isClickable(node /*: any */) { + if (!node) { + return false; + } + const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA']; + const tagName = (node.tagName || '').toUpperCase(); + return clickableTags.includes(tagName) || isClickable(node.parentNode); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js index 3f7d5e84ac7..783fdec65b0 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js @@ -19,9 +19,11 @@ */ // @flow import React from 'react'; -import Avatar from '../../../components/ui/Avatar'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetAssigneePopup from '../popups/SetAssigneePopup'; +import Avatar from '../../../components/ui/Avatar'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -43,6 +45,10 @@ export default class IssueAssign extends React.PureComponent { this.props.togglePopup('assign', open); }; + handleClose = () => { + this.toggleAssign(false); + }; + renderAssignee() { const { issue } = this.props; return ( @@ -67,24 +73,26 @@ export default class IssueAssign extends React.PureComponent { render() { if (this.props.canAssign) { return ( - <BubblePopupHelper - isOpen={this.props.isOpen && this.props.canAssign} - position="bottomleft" - togglePopup={this.toggleAssign} - popup={ - <SetAssigneePopup - issue={this.props.issue} - onFail={this.props.onFail} - onSelect={this.props.onAssign} - /> - }> - <button - className="button-link issue-action issue-action-with-options js-issue-assign" - onClick={this.toggleAssign}> - {this.renderAssignee()} - <i className="little-spacer-left icon-dropdown" /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + closeOnEscape={true} + onRequestClose={this.handleClose} + open={this.props.isOpen && this.props.canAssign} + overlay={ + <SetAssigneePopup + issue={this.props.issue} + onFail={this.props.onFail} + onSelect={this.props.onAssign} + /> + }> + <Button + className="button-link issue-action issue-action-with-options js-issue-assign" + onClick={this.toggleAssign}> + {this.renderAssignee()} + <DropdownIcon className="little-spacer-left" /> + </Button> + </Toggler> + </div> ); } else { return this.renderAssignee(); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js index 43f5adc2d91..a70f6ca5a33 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js @@ -19,11 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import ChangelogPopup from '../popups/ChangelogPopup'; import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Toggler from '../../../components/controls/Toggler'; import Tooltip from '../../../components/controls/Tooltip'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -39,35 +40,39 @@ type Props = { export default class IssueChangelog extends React.PureComponent { /*:: props: Props; */ - handleClick = (evt /*: SyntheticInputEvent */) => { - evt.preventDefault(); + toggleChangelog = (open /*: boolean | void */) => { + this.props.togglePopup('changelog', open); + }; + + handleClick = () => { this.toggleChangelog(); }; - toggleChangelog = (open /*: boolean | void */) => { - this.props.togglePopup('changelog', open); + handleClose = () => { + this.toggleChangelog(false); }; render() { return ( - <BubblePopupHelper - isOpen={this.props.isOpen} - position="bottomright" - togglePopup={this.toggleChangelog} - popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}> - <Tooltip - mouseEnterDelay={0.5} - overlay={<DateTimeFormatter date={this.props.creationDate} />}> - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - onClick={this.handleClick}> - <span className="issue-meta-label"> - <DateFromNow date={this.props.creationDate} /> - </span> - <i className="icon-dropdown little-spacer-left" /> - </button> - </Tooltip> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen} + overlay={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}> + <Tooltip + mouseEnterDelay={0.5} + overlay={<DateTimeFormatter date={this.props.creationDate} />}> + <Button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={this.handleClick}> + <span className="issue-meta-label"> + <DateFromNow date={this.props.creationDate} /> + </span> + <i className="icon-dropdown little-spacer-left" /> + </Button> + </Tooltip> + </Toggler> + </div> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js index 01ba1f984a1..814d4053cf4 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js @@ -20,7 +20,8 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Toggler from '../../../components/controls/Toggler'; +import { Button } from '../../../components/ui/buttons'; import CommentPopup from '../popups/CommentPopup'; import { addIssueComment } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; @@ -49,29 +50,33 @@ export default class IssueCommentAction extends React.PureComponent { this.props.toggleComment(false); }; - handleCommentClick = () => this.props.toggleComment(); + handleCommentClick = () => { + this.props.toggleComment(); + }; + + handleClose = () => { + this.props.toggleComment(false); + }; render() { return ( - <li className="issue-meta"> - <BubblePopupHelper - isOpen={this.props.currentPopup === 'comment'} - position="bottomleft" - togglePopup={this.props.toggleComment} - popup={ + <li className="issue-meta dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.currentPopup === 'comment'} + overlay={ <CommentPopup - customClass="issue-comment-bubble-popup" - placeholder={this.props.commentPlaceholder} onComment={this.addComment} + placeholder={this.props.commentPlaceholder} toggleComment={this.props.toggleComment} /> }> - <button + <Button className="button-link issue-action js-issue-comment" onClick={this.handleCommentClick}> <span className="issue-meta-label">{translate('issue.comment.formlink')}</span> - </button> - </BubblePopupHelper> + </Button> + </Toggler> </li> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js index 1773bd16a0c..3a0910f6552 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import Avatar from '../../../components/ui/Avatar'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Toggler from '../../../components/controls/Toggler'; import EditIcon from '../../../components/icons-components/EditIcon'; import { EditButton, DeleteButton } from '../../../components/ui/buttons'; import CommentDeletePopup from '../popups/CommentDeletePopup'; @@ -48,12 +48,6 @@ export default class IssueCommentLine extends React.PureComponent { openPopup: '' }; - handleCommentClick = (event /*: Event & {target: HTMLElement}*/) => { - if (event.target.tagName === 'A') { - event.stopPropagation(); - } - }; - handleEdit = (text /*: string */) => { this.props.onEdit(this.props.comment.key, text); this.toggleEditPopup(false); @@ -75,9 +69,17 @@ export default class IssueCommentLine extends React.PureComponent { }); }; - toggleDeletePopup = (force /*: ?boolean */) => this.togglePopup('delete', force); + toggleDeletePopup = (force /*: ?boolean */) => { + this.togglePopup('delete', force); + }; - toggleEditPopup = (force /*: ?boolean */) => this.togglePopup('edit', force); + toggleEditPopup = (force /*: ?boolean */) => { + this.togglePopup('edit', force); + }; + + closePopups = () => { + this.setState({ openPopup: '' }); + }; render() { const { comment } = this.props; @@ -95,49 +97,45 @@ export default class IssueCommentLine extends React.PureComponent { <div className="issue-comment-text markdown" dangerouslySetInnerHTML={{ __html: comment.htmlText }} - onClick={this.handleCommentClick} - role="Listitem" - tabIndex={0} /> <div className="issue-comment-age"> <DateFromNow date={comment.createdAt} /> </div> <div className="issue-comment-actions"> {comment.updatable && ( - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={this.state.openPopup === 'edit'} - offset={{ vertical: 0, horizontal: -6 }} - position="bottomright" - togglePopup={this.toggleDeletePopup} - popup={ - <CommentPopup - comment={comment} - customClass="issue-edit-comment-bubble-popup" - onComment={this.handleEdit} - placeholder="" - toggleComment={this.toggleEditPopup} + <div className="dropdown"> + <Toggler + className="display-inline-block" + onRequestClose={this.closePopups} + open={this.state.openPopup === 'edit'} + overlay={ + <CommentPopup + comment={comment} + onComment={this.handleEdit} + placeholder="" + toggleComment={this.toggleEditPopup} + /> + }> + <EditButton + className="js-issue-comment-edit button-small" + onClick={this.toggleEditPopup} /> - }> - <EditButton - className="js-issue-comment-edit button-small" - onClick={this.toggleEditPopup} - /> - </BubblePopupHelper> + </Toggler> + </div> )} {comment.updatable && ( - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={this.state.openPopup === 'delete'} - offset={{ vertical: 0, horizontal: -10 }} - position="bottomright" - togglePopup={this.toggleDeletePopup} - popup={<CommentDeletePopup onDelete={this.handleDelete} />}> - <DeleteButton - className="js-issue-comment-delete button-small" - onClick={this.toggleDeletePopup} - /> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + className="display-inline-block" + onRequestClose={this.closePopups} + open={this.state.openPopup === 'delete'} + overlay={<CommentDeletePopup onDelete={this.handleDelete} />}> + <DeleteButton + className="js-issue-comment-delete button-small" + onClick={this.toggleDeletePopup} + /> + </Toggler> + </div> )} </div> </div> diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js index 888d7f2d521..1f8a9cb2ae5 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js @@ -38,7 +38,6 @@ export default class IssueMessage extends React.PureComponent { handleClick = (e /*: MouseEvent */) => { e.preventDefault(); - e.stopPropagation(); this.context.workspace.openRule({ key: this.props.rule, organization: this.props.organization diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js index 326e64e64ab..f75e0e8f3c7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js @@ -19,10 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetSeverityPopup from '../popups/SetSeverityPopup'; -import SeverityHelper from '../../../components/shared/SeverityHelper'; import { setIssueSeverity } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import SeverityHelper from '../../../components/shared/SeverityHelper'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -42,28 +44,31 @@ export default class IssueSeverity extends React.PureComponent { this.props.togglePopup('set-severity', open); }; - setSeverity = (severity /*: string */) => + setSeverity = (severity /*: string */) => { this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity); + }; + + handleClose = () => { + this.toggleSetSeverity(false); + }; render() { const { issue } = this.props; if (this.props.canSetSeverity) { return ( - <BubblePopupHelper - isOpen={this.props.isOpen && this.props.canSetSeverity} - position="bottomleft" - togglePopup={this.toggleSetSeverity} - popup={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}> - <button - className="button-link issue-action issue-action-with-options js-issue-set-severity" - onClick={this.toggleSetSeverity}> - <SeverityHelper - className="issue-meta-label little-spacer-right" - severity={issue.severity} - /> - <i className="little-spacer-left icon-dropdown" /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen && this.props.canSetSeverity} + overlay={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-severity" + onClick={this.toggleSetSeverity}> + <SeverityHelper className="issue-meta-label" severity={issue.severity} /> + <DropdownIcon className="little-spacer-left" /> + </Button> + </Toggler> + </div> ); } else { return <SeverityHelper className="issue-meta-label" severity={issue.severity} />; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js index fb183a7f8b7..b9f1fcba0be 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js @@ -20,10 +20,11 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetIssueTagsPopup from '../popups/SetIssueTagsPopup'; -import TagsList from '../../../components/tags/TagsList'; import { setIssueTags } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import TagsList from '../../../components/tags/TagsList'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -57,32 +58,39 @@ export default class IssueTags extends React.PureComponent { ); }; + handleClose = () => { + this.toggleSetTags(false); + }; + render() { const { issue } = this.props; const { tags = [] } = issue; if (this.props.canSetTags) { return ( - <BubblePopupHelper - isOpen={this.props.isOpen} - popup={ - <SetIssueTagsPopup - organization={issue.projectOrganization} - selectedTags={tags} - setTags={this.setTags} - /> - } - position="bottomright" - togglePopup={this.toggleSetTags}> - <button - className={'js-issue-edit-tags button-link issue-action issue-action-with-options'} - onClick={this.toggleSetTags}> - <TagsList - allowUpdate={this.props.canSetTags} - tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} - /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen} + overlay={ + <SetIssueTagsPopup + organization={issue.projectOrganization} + selectedTags={tags} + setTags={this.setTags} + /> + }> + <Button + className={'js-issue-edit-tags button-link issue-action issue-action-with-options'} + onClick={this.toggleSetTags}> + <TagsList + allowUpdate={this.props.canSetTags} + tags={ + issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')] + } + /> + </Button> + </Toggler> + </div> ); } else { return ( diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js index 0dad342b1b5..781eebd1f06 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -45,8 +45,6 @@ type Props = {| |}; */ -const stopPropagation = (event /*: Event */) => event.stopPropagation(); - export default function IssueTitleBar(props /*: Props */) { const { issue } = props; const hasSimilarIssuesFilter = props.onFilter != null; @@ -103,7 +101,7 @@ export default function IssueTitleBar(props /*: Props */) { {displayLocations && ( <li className="issue-meta"> {props.displayLocationsLink ? ( - <Link onClick={stopPropagation} target="_blank" to={issueUrl}> + <Link target="_blank" to={issueUrl}> {locationsBadge} </Link> ) : ( @@ -112,11 +110,7 @@ export default function IssueTitleBar(props /*: Props */) { </li> )} <li className="issue-meta"> - <Link - className="js-issue-permalink link-no-underline" - onClick={stopPropagation} - target="_blank" - to={issueUrl}> + <Link className="js-issue-permalink link-no-underline" target="_blank" to={issueUrl}> <LinkIcon /> </Link> </li> diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js index 28379cde1cd..3aced6f8685 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js @@ -20,10 +20,12 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetTransitionPopup from '../popups/SetTransitionPopup'; -import StatusHelper from '../../../components/shared/StatusHelper'; import { setIssueTransition } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import StatusHelper from '../../../components/shared/StatusHelper'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -53,36 +55,41 @@ export default class IssueTransition extends React.PureComponent { this.props.togglePopup('transition', open); }; + handleClose = () => { + this.toggleSetTransition(false); + }; + render() { const { issue } = this.props; if (this.props.hasTransitions) { return ( - <BubblePopupHelper - isOpen={this.props.isOpen && this.props.hasTransitions} - position="bottomleft" - togglePopup={this.toggleSetTransition} - popup={ - <SetTransitionPopup transitions={issue.transitions} onSelect={this.setTransition} /> - }> - <button - className="button-link issue-action issue-action-with-options js-issue-transition" - onClick={this.toggleSetTransition}> - <StatusHelper - className="issue-meta-label little-spacer-right" - status={issue.status} - resolution={issue.resolution} - /> - <i className="little-spacer-left icon-dropdown" /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen && this.props.hasTransitions} + overlay={ + <SetTransitionPopup onSelect={this.setTransition} transitions={issue.transitions} /> + }> + <Button + className="button-link issue-action issue-action-with-options js-issue-transition" + onClick={this.toggleSetTransition}> + <StatusHelper + className="issue-meta-label" + resolution={issue.resolution} + status={issue.status} + /> + <DropdownIcon className="little-spacer-left" /> + </Button> + </Toggler> + </div> ); } else { return ( <StatusHelper className="issue-meta-label" - status={issue.status} resolution={issue.resolution} + status={issue.status} /> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.js b/server/sonar-web/src/main/js/components/issue/components/IssueType.js index 60941288e4b..380e8668a32 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueType.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.js @@ -19,10 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; -import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SetTypePopup from '../popups/SetTypePopup'; import { setIssueType } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -43,26 +45,32 @@ export default class IssueType extends React.PureComponent { this.props.togglePopup('set-type', open); }; - setType = (type /*: string */) => + setType = (type /*: string */) => { this.props.setIssueProperty('type', 'set-type', setIssueType, type); + }; + + handleClose = () => { + this.toggleSetType(false); + }; render() { const { issue } = this.props; if (this.props.canSetSeverity) { return ( - <BubblePopupHelper - isOpen={this.props.isOpen && this.props.canSetSeverity} - position="bottomleft" - togglePopup={this.toggleSetType} - popup={<SetTypePopup issue={issue} onSelect={this.setType} />}> - <button - className="button-link issue-action issue-action-with-options js-issue-set-type" - onClick={this.toggleSetType}> - <IssueTypeIcon className="little-spacer-right" query={issue.type} /> - {translate('issue.type', issue.type)} - <i className="little-spacer-left icon-dropdown" /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen && this.props.canSetSeverity} + overlay={<SetTypePopup issue={issue} onSelect={this.setType} />}> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-type" + onClick={this.toggleSetType}> + <IssueTypeIcon className="little-spacer-right" query={issue.type} /> + {translate('issue.type', issue.type)} + <DropdownIcon className="little-spacer-left" /> + </Button> + </Toggler> + </div> ); } else { return ( diff --git a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js index b9da955025a..5b900ac2e48 100644 --- a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js +++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js @@ -19,8 +19,10 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SimilarIssuesPopup from '../popups/SimilarIssuesPopup'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -51,20 +53,26 @@ export default class SimilarIssuesFilter extends React.PureComponent { this.props.togglePopup('similarIssues', open); }; + handleClose = () => { + this.togglePopup(false); + }; + render() { return ( - <BubblePopupHelper - isOpen={this.props.isOpen} - position="bottomright" - togglePopup={this.togglePopup} - popup={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}> - <button - className="js-issue-filter button-link issue-action issue-action-with-options" - aria-label={translate('issue.filter_similar_issues')} - onClick={this.handleClick}> - <i className="icon-filter icon-half-transparent" /> <i className="icon-dropdown" /> - </button> - </BubblePopupHelper> + <div className="dropdown"> + <Toggler + onRequestClose={this.handleClose} + open={this.props.isOpen} + overlay={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}> + <Button + aria-label={translate('issue.filter_similar_issues')} + className="js-issue-filter button-link issue-action issue-action-with-options" + onClick={this.handleClick}> + <i className="icon-filter icon-half-transparent" /> + <DropdownIcon className="little-spacer-left" /> + </Button> + </Toggler> + </div> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js index cdd7cfeb6f1..ad39643695e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js @@ -34,8 +34,8 @@ it('should render without the action when the correct rights are missing', () => canAssign={false} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={jest.fn()} /> ); @@ -48,8 +48,8 @@ it('should render with the action', () => { canAssign={true} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={jest.fn()} /> ); @@ -63,12 +63,12 @@ it('should open the popup when the button is clicked', () => { canAssign={true} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js index 0b2b9071cd8..8e03fdef42a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -52,7 +52,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js index d54334a61f9..2114f366226 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js @@ -25,8 +25,8 @@ import { click } from '../../../../helpers/testUtils'; it('should render correctly', () => { const element = shallow( <IssueCommentAction - issueKey="issue-key" currentPopup={null} + issueKey="issue-key" onFail={jest.fn()} onIssueChange={jest.fn()} toggleComment={jest.fn()} @@ -39,14 +39,14 @@ it('should open the popup when the button is clicked', () => { const toggle = jest.fn(); const element = shallow( <IssueCommentAction - issueKey="issue-key" currentPopup={null} + issueKey="issue-key" onFail={jest.fn()} onIssueChange={jest.fn()} toggleComment={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls.length).toBe(1); element.setProps({ currentPopup: 'comment' }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js index a321192729a..90cc0e1d8dc 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js @@ -53,9 +53,9 @@ it('should open the right popups when the buttons are clicked', () => { const element = shallow( <IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} /> ); - element.find('.js-issue-comment-edit').prop('onClick')(); + click(element.find('.js-issue-comment-edit')); expect(element.state()).toMatchSnapshot(); - element.find('.js-issue-comment-delete').prop('onClick')(); + click(element.find('.js-issue-comment-delete')); expect(element.state()).toMatchSnapshot(); element.update(); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js index 98e4cf45c5b..08b00404347 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js @@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js index aa353241a8a..81d9a512a45 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js @@ -71,7 +71,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js index 9bfc5d7cd85..98da66e0449 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js @@ -84,7 +84,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js index de6dcde42bf..51132f338ca 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js @@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap index d4ff90579c6..d768c0c852d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap @@ -4,112 +4,111 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "assign", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <Connect(SetAssigneePopup) - issue={ - Object { - "assignee": "john", - "assigneeAvatar": "gravatarhash", - "assigneeName": "John Doe", - } - } - onFail={[MockFunction]} - onSelect={[MockFunction]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-assign" - onClick={[Function]} + <Toggler + closeOnEscape={true} + onRequestClose={[Function]} + open={true} + overlay={ + <Connect(SetAssigneePopup) + issue={ + Object { + "assignee": "john", + "assigneeAvatar": "gravatarhash", + "assigneeName": "John Doe", + } + } + onFail={[MockFunction]} + onSelect={[MockFunction]} + /> + } > - <span> - <span - className="text-top" - > - <Connect(Avatar) - className="little-spacer-right" - hash="gravatarhash" - name="John Doe" - size={16} - /> + <Button + className="button-link issue-action issue-action-with-options js-issue-assign" + onClick={[Function]} + > + <span> + <span + className="text-top" + > + <Connect(Avatar) + className="little-spacer-right" + hash="gravatarhash" + name="John Doe" + size={16} + /> + </span> + <span + className="issue-meta-label" + > + John Doe + </span> </span> - <span - className="issue-meta-label" - > - John Doe - </span> - </span> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render with the action 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <Connect(SetAssigneePopup) - issue={ - Object { - "assignee": "john", - "assigneeAvatar": "gravatarhash", - "assigneeName": "John Doe", - } - } - onFail={[MockFunction]} - onSelect={[MockFunction]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-assign" - onClick={[Function]} + <Toggler + closeOnEscape={true} + onRequestClose={[Function]} + open={false} + overlay={ + <Connect(SetAssigneePopup) + issue={ + Object { + "assignee": "john", + "assigneeAvatar": "gravatarhash", + "assigneeName": "John Doe", + } + } + onFail={[MockFunction]} + onSelect={[MockFunction]} + /> + } > - <span> - <span - className="text-top" - > - <Connect(Avatar) - className="little-spacer-right" - hash="gravatarhash" - name="John Doe" - size={16} - /> + <Button + className="button-link issue-action issue-action-with-options js-issue-assign" + onClick={[Function]} + > + <span> + <span + className="text-top" + > + <Connect(Avatar) + className="little-spacer-right" + hash="gravatarhash" + name="John Doe" + size={16} + /> + </span> + <span + className="issue-meta-label" + > + John Doe + </span> </span> - <span - className="issue-meta-label" - > - John Doe - </span> - </span> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap index 97f8c58d32a..0628de785bf 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap @@ -10,91 +10,97 @@ Array [ `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <ChangelogPopup - issue={ - Object { - "author": "john.david.dalton@gmail.com", - "creationDate": "2017-03-01T09:36:01+0100", - "key": "issuekey", - } - } - onFail={[MockFunction]} - /> - } - position="bottomright" - togglePopup={[Function]} +<div + className="dropdown" > - <Tooltip - mouseEnterDelay={0.5} + <Toggler + onRequestClose={[Function]} + open={true} overlay={ - <DateTimeFormatter - date="2017-03-01T09:36:01+0100" + <ChangelogPopup + issue={ + Object { + "author": "john.david.dalton@gmail.com", + "creationDate": "2017-03-01T09:36:01+0100", + "key": "issuekey", + } + } + onFail={[MockFunction]} /> } > - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - onClick={[Function]} + <Tooltip + mouseEnterDelay={0.5} + overlay={ + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> + } > - <span - className="issue-meta-label" + <Button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={[Function]} > - <DateFromNow - date="2017-03-01T09:36:01+0100" + <span + className="issue-meta-label" + > + <DateFromNow + date="2017-03-01T09:36:01+0100" + /> + </span> + <i + className="icon-dropdown little-spacer-left" /> - </span> - <i - className="icon-dropdown little-spacer-left" - /> - </button> - </Tooltip> -</BubblePopupHelper> + </Button> + </Tooltip> + </Toggler> +</div> `; exports[`should render correctly 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <ChangelogPopup - issue={ - Object { - "author": "john.david.dalton@gmail.com", - "creationDate": "2017-03-01T09:36:01+0100", - "key": "issuekey", - } - } - onFail={[MockFunction]} - /> - } - position="bottomright" - togglePopup={[Function]} +<div + className="dropdown" > - <Tooltip - mouseEnterDelay={0.5} + <Toggler + onRequestClose={[Function]} + open={false} overlay={ - <DateTimeFormatter - date="2017-03-01T09:36:01+0100" + <ChangelogPopup + issue={ + Object { + "author": "john.david.dalton@gmail.com", + "creationDate": "2017-03-01T09:36:01+0100", + "key": "issuekey", + } + } + onFail={[MockFunction]} /> } > - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - onClick={[Function]} + <Tooltip + mouseEnterDelay={0.5} + overlay={ + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> + } > - <span - className="issue-meta-label" + <Button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={[Function]} > - <DateFromNow - date="2017-03-01T09:36:01+0100" + <span + className="issue-meta-label" + > + <DateFromNow + date="2017-03-01T09:36:01+0100" + /> + </span> + <i + className="icon-dropdown little-spacer-left" /> - </span> - <i - className="icon-dropdown little-spacer-left" - /> - </button> - </Tooltip> -</BubblePopupHelper> + </Button> + </Tooltip> + </Toggler> +</div> `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap index 7305e5d8472..3c62307690e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap @@ -2,13 +2,13 @@ exports[`should open the popup when the button is clicked 1`] = ` <li - className="issue-meta" + className="issue-meta dropdown" > - <BubblePopupHelper - isOpen={true} - popup={ + <Toggler + onRequestClose={[Function]} + open={true} + overlay={ <CommentPopup - customClass="issue-comment-bubble-popup" onComment={[Function]} placeholder={undefined} toggleComment={ @@ -20,16 +20,8 @@ exports[`should open the popup when the button is clicked 1`] = ` } /> } - position="bottomleft" - togglePopup={ - [MockFunction] { - "calls": Array [ - Array [], - ], - } - } > - <button + <Button className="button-link issue-action js-issue-comment" onClick={[Function]} > @@ -38,29 +30,27 @@ exports[`should open the popup when the button is clicked 1`] = ` > issue.comment.formlink </span> - </button> - </BubblePopupHelper> + </Button> + </Toggler> </li> `; exports[`should render correctly 1`] = ` <li - className="issue-meta" + className="issue-meta dropdown" > - <BubblePopupHelper - isOpen={false} - popup={ + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ <CommentPopup - customClass="issue-comment-bubble-popup" onComment={[Function]} placeholder={undefined} toggleComment={[MockFunction]} /> } - position="bottomleft" - togglePopup={[MockFunction]} > - <button + <Button className="button-link issue-action js-issue-comment" onClick={[Function]} > @@ -69,7 +59,7 @@ exports[`should render correctly 1`] = ` > issue.comment.formlink </span> - </button> - </BubblePopupHelper> + </Button> + </Toggler> </li> `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap index 71f4ef39f22..4db5ed5aa9c 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap @@ -35,9 +35,6 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` "__html": "<b>test</b>", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} /> <div className="issue-comment-age" @@ -49,63 +46,56 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` <div className="issue-comment-actions" > - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={false} - offset={ - Object { - "horizontal": -6, - "vertical": 0, - } - } - popup={ - <CommentPopup - comment={ - Object { - "authorAvatar": "gravatarhash", - "authorName": "John Doe", - "createdAt": "2017-03-01T09:36:01+0100", - "htmlText": "<b>test</b>", - "key": "comment-key", - "updatable": true, + <div + className="dropdown" + > + <Toggler + className="display-inline-block" + onRequestClose={[Function]} + open={false} + overlay={ + <CommentPopup + comment={ + Object { + "authorAvatar": "gravatarhash", + "authorName": "John Doe", + "createdAt": "2017-03-01T09:36:01+0100", + "htmlText": "<b>test</b>", + "key": "comment-key", + "updatable": true, + } } - } - customClass="issue-edit-comment-bubble-popup" - onComment={[Function]} - placeholder="" - toggleComment={[Function]} + onComment={[Function]} + placeholder="" + toggleComment={[Function]} + /> + } + > + <EditButton + className="js-issue-comment-edit button-small" + onClick={[Function]} /> - } - position="bottomright" - togglePopup={[Function]} + </Toggler> + </div> + <div + className="dropdown" > - <EditButton - className="js-issue-comment-edit button-small" - onClick={[Function]} - /> - </BubblePopupHelper> - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={true} - offset={ - Object { - "horizontal": -10, - "vertical": 0, + <Toggler + className="display-inline-block" + onRequestClose={[Function]} + open={true} + overlay={ + <CommentDeletePopup + onDelete={[Function]} + /> } - } - popup={ - <CommentDeletePopup - onDelete={[Function]} + > + <DeleteButton + className="js-issue-comment-delete button-small" + onClick={[Function]} /> - } - position="bottomright" - togglePopup={[Function]} - > - <DeleteButton - className="js-issue-comment-delete button-small" - onClick={[Function]} - /> - </BubblePopupHelper> + </Toggler> + </div> </div> </div> `; @@ -133,9 +123,6 @@ exports[`should render correctly a comment that is not updatable 1`] = ` "__html": "<b>test</b>", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} /> <div className="issue-comment-age" @@ -173,9 +160,6 @@ exports[`should render correctly a comment that is updatable 1`] = ` "__html": "<b>test</b>", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} /> <div className="issue-comment-age" @@ -187,63 +171,56 @@ exports[`should render correctly a comment that is updatable 1`] = ` <div className="issue-comment-actions" > - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={false} - offset={ - Object { - "horizontal": -6, - "vertical": 0, - } - } - popup={ - <CommentPopup - comment={ - Object { - "authorAvatar": "gravatarhash", - "authorName": "John Doe", - "createdAt": "2017-03-01T09:36:01+0100", - "htmlText": "<b>test</b>", - "key": "comment-key", - "updatable": true, + <div + className="dropdown" + > + <Toggler + className="display-inline-block" + onRequestClose={[Function]} + open={false} + overlay={ + <CommentPopup + comment={ + Object { + "authorAvatar": "gravatarhash", + "authorName": "John Doe", + "createdAt": "2017-03-01T09:36:01+0100", + "htmlText": "<b>test</b>", + "key": "comment-key", + "updatable": true, + } } - } - customClass="issue-edit-comment-bubble-popup" - onComment={[Function]} - placeholder="" - toggleComment={[Function]} + onComment={[Function]} + placeholder="" + toggleComment={[Function]} + /> + } + > + <EditButton + className="js-issue-comment-edit button-small" + onClick={[Function]} /> - } - position="bottomright" - togglePopup={[Function]} + </Toggler> + </div> + <div + className="dropdown" > - <EditButton - className="js-issue-comment-edit button-small" - onClick={[Function]} - /> - </BubblePopupHelper> - <BubblePopupHelper - className="bubble-popup-helper-inline" - isOpen={false} - offset={ - Object { - "horizontal": -10, - "vertical": 0, + <Toggler + className="display-inline-block" + onRequestClose={[Function]} + open={false} + overlay={ + <CommentDeletePopup + onDelete={[Function]} + /> } - } - popup={ - <CommentDeletePopup - onDelete={[Function]} + > + <DeleteButton + className="js-issue-comment-delete button-small" + onClick={[Function]} /> - } - position="bottomright" - togglePopup={[Function]} - > - <DeleteButton - className="js-issue-comment-delete button-small" - onClick={[Function]} - /> - </BubblePopupHelper> + </Toggler> + </div> </div> </div> `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap index 98eb8644b75..898a75fd3d6 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap @@ -4,80 +4,77 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "set-severity", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <SetSeverityPopup - issue={ - Object { - "severity": "BLOCKER", - } - } - onSelect={[Function]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-set-severity" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={true} + overlay={ + <SetSeverityPopup + issue={ + Object { + "severity": "BLOCKER", + } + } + onSelect={[Function]} + /> + } > - <SeverityHelper - className="issue-meta-label little-spacer-right" - severity="BLOCKER" - /> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-severity" + onClick={[Function]} + > + <SeverityHelper + className="issue-meta-label" + severity="BLOCKER" + /> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render with the action 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <SetSeverityPopup - issue={ - Object { - "severity": "BLOCKER", - } - } - onSelect={[Function]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-set-severity" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <SetSeverityPopup + issue={ + Object { + "severity": "BLOCKER", + } + } + onSelect={[Function]} + /> + } > - <SeverityHelper - className="issue-meta-label little-spacer-right" - severity="BLOCKER" - /> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-severity" + onClick={[Function]} + > + <SeverityHelper + className="issue-meta-label" + severity="BLOCKER" + /> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap index fa30aff7c20..ca33fb7f1ec 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap @@ -4,88 +4,85 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "edit-tags", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <SetIssueTagsPopup - organization="foo" - selectedTags={ - Array [ - "mytag", - "test", - ] - } - setTags={[Function]} - /> - } - position="bottomright" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="js-issue-edit-tags button-link issue-action issue-action-with-options" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={true} + overlay={ + <SetIssueTagsPopup + organization="foo" + selectedTags={ + Array [ + "mytag", + "test", + ] + } + setTags={[Function]} + /> + } > - <TagsList - allowUpdate={true} - tags={ - Array [ - "mytag", - "test", - ] - } - /> - </button> -</BubblePopupHelper> + <Button + className="js-issue-edit-tags button-link issue-action issue-action-with-options" + onClick={[Function]} + > + <TagsList + allowUpdate={true} + tags={ + Array [ + "mytag", + "test", + ] + } + /> + </Button> + </Toggler> +</div> `; exports[`should render with the action 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <SetIssueTagsPopup - organization="foo" - selectedTags={ - Array [ - "mytag", - "test", - ] - } - setTags={[Function]} - /> - } - position="bottomright" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="js-issue-edit-tags button-link issue-action issue-action-with-options" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <SetIssueTagsPopup + organization="foo" + selectedTags={ + Array [ + "mytag", + "test", + ] + } + setTags={[Function]} + /> + } > - <TagsList - allowUpdate={true} - tags={ - Array [ - "mytag", - "test", - ] - } - /> - </button> -</BubblePopupHelper> + <Button + className="js-issue-edit-tags button-link issue-action issue-action-with-options" + onClick={[Function]} + > + <TagsList + allowUpdate={true} + tags={ + Array [ + "mytag", + "test", + ] + } + /> + </Button> + </Toggler> +</div> `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap index 30fc9e84c33..8f3694dd8d0 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -68,7 +68,6 @@ exports[`should render the titlebar correctly 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} target="_blank" @@ -152,7 +151,6 @@ exports[`should render the titlebar with the filter 1`] = ` > <Link className="js-issue-permalink link-no-underline" - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} target="_blank" diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap index cd14db53dd2..dcfe4ff35ea 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap @@ -4,118 +4,118 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "transition", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <SetTransitionPopup - onSelect={[Function]} - transitions={ - Array [ - "confirm", - "resolve", - "falsepositive", - "wontfix", - ] - } - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={true} + overlay={ + <SetTransitionPopup + onSelect={[Function]} + transitions={ + Array [ + "confirm", + "resolve", + "falsepositive", + "wontfix", + ] + } + /> + } > - <StatusHelper - className="issue-meta-label little-spacer-right" - status="OPEN" - /> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-transition" + onClick={[Function]} + > + <StatusHelper + className="issue-meta-label" + status="OPEN" + /> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render with a resolution 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <SetTransitionPopup - onSelect={[Function]} - transitions={ - Array [ - "reopen", - ] - } - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <SetTransitionPopup + onSelect={[Function]} + transitions={ + Array [ + "reopen", + ] + } + /> + } > - <StatusHelper - className="issue-meta-label little-spacer-right" - resolution="FIXED" - status="RESOLVED" - /> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-transition" + onClick={[Function]} + > + <StatusHelper + className="issue-meta-label" + resolution="FIXED" + status="RESOLVED" + /> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render with the action 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <SetTransitionPopup - onSelect={[Function]} - transitions={ - Array [ - "confirm", - "resolve", - "falsepositive", - "wontfix", - ] - } - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <SetTransitionPopup + onSelect={[Function]} + transitions={ + Array [ + "confirm", + "resolve", + "falsepositive", + "wontfix", + ] + } + /> + } > - <StatusHelper - className="issue-meta-label little-spacer-right" - status="OPEN" - /> - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-transition" + onClick={[Function]} + > + <StatusHelper + className="issue-meta-label" + status="OPEN" + /> + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render without the action when there is no transitions 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap index 77988d1f003..1eb2844dbf6 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap @@ -4,82 +4,79 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "set-type", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` -<BubblePopupHelper - isOpen={true} - popup={ - <SetTypePopup - issue={ - Object { - "type": "bug", - } - } - onSelect={[Function]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-set-type" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={true} + overlay={ + <SetTypePopup + issue={ + Object { + "type": "bug", + } + } + onSelect={[Function]} + /> + } > - <IssueTypeIcon - className="little-spacer-right" - query="bug" - /> - issue.type.bug - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-type" + onClick={[Function]} + > + <IssueTypeIcon + className="little-spacer-right" + query="bug" + /> + issue.type.bug + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render with the action 1`] = ` -<BubblePopupHelper - isOpen={false} - popup={ - <SetTypePopup - issue={ - Object { - "type": "bug", - } - } - onSelect={[Function]} - /> - } - position="bottomleft" - togglePopup={[Function]} +<div + className="dropdown" > - <button - className="button-link issue-action issue-action-with-options js-issue-set-type" - onClick={[Function]} + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <SetTypePopup + issue={ + Object { + "type": "bug", + } + } + onSelect={[Function]} + /> + } > - <IssueTypeIcon - className="little-spacer-right" - query="bug" - /> - issue.type.bug - <i - className="little-spacer-left icon-dropdown" - /> - </button> -</BubblePopupHelper> + <Button + className="button-link issue-action issue-action-with-options js-issue-set-type" + onClick={[Function]} + > + <IssueTypeIcon + className="little-spacer-right" + query="bug" + /> + issue.type.bug + <DropdownIcon + className="little-spacer-left" + /> + </Button> + </Toggler> +</div> `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js index b3f230810fe..fcfd515b587 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js @@ -22,9 +22,9 @@ import React from 'react'; import { getIssueChangelog } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; import Avatar from '../../../components/ui/Avatar'; -import BubblePopup from '../../../components/common/BubblePopup'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../components/IssueChangelogDiff'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { ChangelogDiff } from '../components/IssueChangelogDiff'; */ /*:: import type { Issue } from '../types'; */ @@ -80,8 +80,8 @@ export default class ChangelogPopup extends React.PureComponent { const { issue } = this.props; const { author } = issue; return ( - <BubblePopup position={this.props.popupPosition} customClass="bubble-popup-bottom-right"> - <div className="issue-changelog"> + <DropdownOverlay> + <div className="menu is-container issue-changelog"> <table className="spaced"> <tbody> <tr> @@ -110,14 +110,14 @@ export default class ChangelogPopup extends React.PureComponent { {item.userName} </p> )} - {item.diffs.map(diff => <IssueChangelogDiff key={diff.key} diff={diff} />)} + {item.diffs.map(diff => <IssueChangelogDiff diff={diff} key={diff.key} />)} </td> </tr> ))} </tbody> </table> </div> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js index 9776ab00253..c7fd5d89ae8 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js @@ -19,8 +19,9 @@ */ // @flow import React from 'react'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; -import BubblePopup from '../../../components/common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: type Props = { @@ -31,13 +32,13 @@ type Props = { export default function CommentDeletePopup(props /*: Props */) { return ( - <BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom-right"> - <div className="text-right"> + <DropdownOverlay> + <div className="menu is-container"> <div className="spacer-bottom">{translate('issue.comment.delete_confirm_message')}</div> - <button className="button-red" onClick={props.onDelete}> + <Button className="button-red" onClick={props.onDelete}> {translate('delete')} - </button> + </Button> </div> - </BubblePopup> + </DropdownOverlay> ); } diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js index 2a17f71383b..bee719a083e 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js @@ -19,16 +19,15 @@ */ // @flow import React from 'react'; -import classNames from 'classnames'; -import BubblePopup from '../../../components/common/BubblePopup'; import MarkdownTips from '../../../components/common/MarkdownTips'; +import { Button, ResetButtonLink } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { IssueComment } from '../types'; */ /*:: type Props = { comment?: IssueComment, - customClass?: string, onComment: string => void, toggleComment: boolean => void, placeholder: string, @@ -63,8 +62,7 @@ export default class CommentPopup extends React.PureComponent { } }; - handleCancelClick = (evt /*: MouseEvent */) => { - evt.preventDefault(); + handleCancelClick = () => { this.props.toggleComment(false); }; @@ -81,37 +79,37 @@ export default class CommentPopup extends React.PureComponent { render() { const { comment } = this.props; return ( - <BubblePopup - position={this.props.popupPosition} - customClass={classNames(this.props.customClass, 'bubble-popup-bottom-right')}> - <div className="issue-comment-form-text"> - <textarea - autoFocus={true} - placeholder={this.props.placeholder} - onChange={this.handleCommentChange} - onKeyDown={this.handleKeyboard} - value={this.state.textComment} - rows="2" - /> - </div> - <div className="issue-comment-form-footer"> - <div className="issue-comment-form-actions"> - <button - className="js-issue-comment-submit little-spacer-right" - disabled={this.state.textComment.trim().length < 1} - onClick={this.handleCommentClick}> - {comment && translate('save')} - {!comment && translate('issue.comment.submit')} - </button> - <a href="#" className="js-issue-comment-cancel" onClick={this.handleCancelClick}> - {translate('cancel')} - </a> + <DropdownOverlay> + <div className="issue-comment-bubble-popup"> + <div className="issue-comment-form-text"> + <textarea + autoFocus={true} + onChange={this.handleCommentChange} + onKeyDown={this.handleKeyboard} + placeholder={this.props.placeholder} + rows="2" + value={this.state.textComment} + /> </div> - <div className="issue-comment-form-tips"> - <MarkdownTips /> + <div className="issue-comment-form-footer"> + <div className="issue-comment-form-actions"> + <Button + className="js-issue-comment-submit little-spacer-right" + disabled={this.state.textComment.trim().length < 1} + onClick={this.handleCommentClick}> + {comment && translate('save')} + {!comment && translate('issue.comment.submit')} + </Button> + <ResetButtonLink className="js-issue-comment-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </ResetButtonLink> + </div> + <div className="issue-comment-form-tips"> + <MarkdownTips /> + </div> </div> </div> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js index 270cbe04158..b5658c997d3 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js @@ -23,7 +23,6 @@ import { map } from 'lodash'; import { connect } from 'react-redux'; import * as PropTypes from 'prop-types'; import Avatar from '../../../components/ui/Avatar'; -import BubblePopup from '../../../components/common/BubblePopup'; import SelectList from '../../../components/common/SelectList'; import SelectListItem from '../../../components/common/SelectListItem'; import SearchBox from '../../../components/controls/SearchBox'; @@ -31,6 +30,7 @@ import { searchMembers } from '../../../api/organizations'; import { searchUsers } from '../../../api/users'; import { translate } from '../../../helpers/l10n'; import { getCurrentUser } from '../../../store/rootReducer'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { Issue } from '../types'; */ /*:: @@ -123,9 +123,7 @@ class SetAssigneePopup extends React.PureComponent { render() { return ( - <BubblePopup - customClass="bubble-popup-menu bubble-popup-bottom" - position={this.props.popupPosition}> + <DropdownOverlay noPadding={true}> <div className="multi-select"> <div className="menu-search"> <SearchBox @@ -142,7 +140,7 @@ class SetAssigneePopup extends React.PureComponent { items={map(this.state.users, 'login')} onSelect={this.props.onSelect}> {this.state.users.map(user => ( - <SelectListItem key={user.login} item={user.login}> + <SelectListItem item={user.login} key={user.login}> {!!user.login && ( <Avatar className="spacer-right" hash={user.avatar} name={user.name} size={16} /> )} @@ -155,7 +153,7 @@ class SetAssigneePopup extends React.PureComponent { ))} </SelectList> </div> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx index 90b8af5f628..c41c216e8b6 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx @@ -19,12 +19,12 @@ */ import * as React from 'react'; import { difference, without } from 'lodash'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import TagsSelector from '../../../components/tags/TagsSelector'; import { searchIssueTags } from '../../../api/issues'; +import { DropdownOverlay } from '../../controls/Dropdown'; +import { PopupPlacement } from '../../ui/popups'; interface Props { - popupPosition: BubblePopupPosition; organization: string; selectedTags: string[]; setTags: (tags: string[]) => void; @@ -74,15 +74,16 @@ export default class SetIssueTagsPopup extends React.PureComponent<Props, State> render() { const availableTags = difference(this.state.searchResult, this.props.selectedTags); return ( - <TagsSelector - listSize={LIST_SIZE} - onSearch={this.onSearch} - onSelect={this.onSelect} - onUnselect={this.onUnselect} - position={this.props.popupPosition} - selectedTags={this.props.selectedTags} - tags={availableTags} - /> + <DropdownOverlay placement={PopupPlacement.BottomRight}> + <TagsSelector + listSize={LIST_SIZE} + onSearch={this.onSearch} + onSelect={this.onSelect} + onUnselect={this.onUnselect} + selectedTags={this.props.selectedTags} + tags={availableTags} + /> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js index cc03b290475..2d3c2232f88 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js @@ -20,17 +20,16 @@ // @flow import React from 'react'; import { translate } from '../../../helpers/l10n'; -import BubblePopup from '../../../components/common/BubblePopup'; import SelectList from '../../../components/common/SelectList'; import SelectListItem from '../../../components/common/SelectListItem'; import SeverityIcon from '../../../components/shared/SeverityIcon'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { Issue } from '../types'; */ /*:: type Props = { issue: Issue, onSelect: string => void, - popupPosition?: {} }; */ @@ -41,21 +40,19 @@ export default class SetSeverityPopup extends React.PureComponent { render() { return ( - <BubblePopup - position={this.props.popupPosition} - customClass="bubble-popup-menu bubble-popup-bottom"> + <DropdownOverlay> <SelectList - items={SEVERITY} currentItem={this.props.issue.severity} + items={SEVERITY} onSelect={this.props.onSelect}> {SEVERITY.map(severity => ( - <SelectListItem key={severity} item={severity}> + <SelectListItem item={severity} key={severity}> <SeverityIcon className="little-spacer-right" severity={severity} /> {translate('severity', severity)} </SelectListItem> ))} </SelectList> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js index 44bf942253d..37d342f7eb2 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js @@ -19,16 +19,15 @@ */ // @flow import React from 'react'; -import BubblePopup from '../../../components/common/BubblePopup'; import SelectList from '../../../components/common/SelectList'; import SelectListItem from '../../../components/common/SelectListItem'; import { translate } from '../../../helpers/l10n'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: type Props = { transitions: Array<string>, onSelect: string => void, - popupPosition?: {} }; */ @@ -38,22 +37,20 @@ export default class SetTransitionPopup extends React.PureComponent { render() { const { transitions } = this.props; return ( - <BubblePopup - position={this.props.popupPosition} - customClass="bubble-popup-menu bubble-popup-bottom"> - <SelectList items={transitions} currentItem={transitions[0]} onSelect={this.props.onSelect}> + <DropdownOverlay> + <SelectList currentItem={transitions[0]} items={transitions} onSelect={this.props.onSelect}> {transitions.map(transition => { return ( <SelectListItem - key={transition} item={transition} + key={transition} title={translate('issue.transition', transition, 'description')}> {translate('issue.transition', transition)} </SelectListItem> ); })} </SelectList> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js index f623afccd08..f69e07cf141 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js @@ -20,17 +20,16 @@ // @flow import React from 'react'; import { translate } from '../../../helpers/l10n'; -import BubblePopup from '../../../components/common/BubblePopup'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SelectList from '../../../components/common/SelectList'; import SelectListItem from '../../../components/common/SelectListItem'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { Issue } from '../types'; */ /*:: type Props = { issue: Issue, onSelect: string => void, - popupPosition?: {} }; */ @@ -41,21 +40,19 @@ export default class SetTypePopup extends React.PureComponent { render() { return ( - <BubblePopup - position={this.props.popupPosition} - customClass="bubble-popup-menu bubble-popup-bottom"> + <DropdownOverlay> <SelectList - items={TYPES} currentItem={this.props.issue.type} + items={TYPES} onSelect={this.props.onSelect}> {TYPES.map(type => ( - <SelectListItem key={type} item={type}> + <SelectListItem item={type} key={type}> <IssueTypeIcon className="little-spacer-right" query={type} /> {translate('issue.type', type)} </SelectListItem> ))} </SelectList> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js index fb9b24780e3..7a28c6594ed 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js @@ -19,9 +19,9 @@ */ // @flow import React from 'react'; -import BubblePopup from '../../../components/common/BubblePopup'; import SelectList from '../../../components/common/SelectList'; import SelectListItem from '../../../components/common/SelectListItem'; +import { DropdownOverlay } from '../../../components/controls/Dropdown'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import StatusHelper from '../../../components/shared/StatusHelper'; import QualifierIcon from '../../../components/shared/QualifierIcon'; @@ -35,7 +35,6 @@ import { fileFromPath, limitComponentName } from '../../../helpers/path'; type Props = {| issue: Issue, onFilter: (property: string, issue: Issue) => void, - popupPosition?: {} |}; */ @@ -64,9 +63,7 @@ export default class SimilarIssuesPopup extends React.PureComponent { ].filter(item => item); return ( - <BubblePopup - position={this.props.popupPosition} - customClass="bubble-popup-menu bubble-popup-bottom-right"> + <DropdownOverlay noPadding={true}> <header className="menu-search"> <h6>{translate('issue.filter_similar_issues')}</h6> </header> @@ -118,7 +115,7 @@ export default class SimilarIssuesPopup extends React.PureComponent { {issue.tags != null && issue.tags.map(tag => ( - <SelectListItem key={`tag###${tag}`} item={`tag###${tag}`}> + <SelectListItem item={`tag###${tag}`} key={`tag###${tag}`}> <i className="icon-tags icon-half-transparent little-spacer-right" /> {tag} </SelectListItem> @@ -143,7 +140,7 @@ export default class SimilarIssuesPopup extends React.PureComponent { {fileFromPath(issue.componentLongName)} </SelectListItem> </SelectList> - </BubblePopup> + </DropdownOverlay> ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js index 2256e3fb35a..be0c222335d 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js @@ -26,6 +26,6 @@ it('should render the comment delete popup correctly', () => { const onDelete = jest.fn(); const element = shallow(<CommentDeletePopup onDelete={onDelete} />); expect(element).toMatchSnapshot(); - click(element.find('button')); + click(element.find('Button')); expect(onDelete.mock.calls.length).toBe(1); }); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js index 7716fe669a3..6cfd0dcdf64 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js @@ -25,10 +25,10 @@ import { click } from '../../../../helpers/testUtils'; it('should render the comment popup correctly without existing comment', () => { const element = shallow( <CommentPopup + customClass="myclass" onComment={jest.fn()} - toggleComment={jest.fn()} placeholder="placeholder test" - customClass="myclass" + toggleComment={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -37,12 +37,10 @@ it('should render the comment popup correctly without existing comment', () => { it('should render the comment popup correctly when changing a comment', () => { const element = shallow( <CommentPopup - comment={{ - markdown: '*test*' - }} + comment={{ markdown: '*test*' }} onComment={jest.fn()} - toggleComment={jest.fn()} placeholder="" + toggleComment={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -52,15 +50,15 @@ it('should render not allow to send comment with only spaces', () => { const onComment = jest.fn(); const element = shallow( <CommentPopup + customClass="myclass" onComment={onComment} - toggleComment={jest.fn()} placeholder="placeholder test" - customClass="myclass" + toggleComment={jest.fn()} /> ); - click(element.find('button.js-issue-comment-submit')); + click(element.find('.js-issue-comment-submit')); expect(onComment.mock.calls.length).toBe(0); element.setState({ textComment: 'mycomment' }); - click(element.find('button.js-issue-comment-submit')); + click(element.find('.js-issue-comment-submit')); expect(onComment.mock.calls.length).toBe(1); }); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx index f66e329ec87..00b85fd1817 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx @@ -23,12 +23,7 @@ import SetIssueTagsPopup from '../SetIssueTagsPopup'; it('should render tags popup correctly', () => { const element = shallow( - <SetIssueTagsPopup - organization="foo" - popupPosition={{}} - selectedTags={['mytag']} - setTags={jest.fn()} - /> + <SetIssueTagsPopup organization="foo" selectedTags={['mytag']} setTags={jest.fn()} /> ); element.setState({ searchResult: ['mytag', 'test', 'second'] }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap index 65cade73cf8..f54cfd88b77 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render the changelog popup correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom-right" -> +<DropdownOverlay> <div - className="issue-changelog" + className="menu is-container issue-changelog" > <table className="spaced" @@ -62,5 +60,5 @@ exports[`should render the changelog popup correctly 1`] = ` </tbody> </table> </div> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap index 634b35907be..43c00928943 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap @@ -1,23 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render the comment delete popup correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom-right" -> +<DropdownOverlay> <div - className="text-right" + className="menu is-container" > <div className="spacer-bottom" > issue.comment.delete_confirm_message </div> - <button + <Button className="button-red" onClick={[MockFunction]} > delete - </button> + </Button> </div> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap index d33898199b7..8ed5e9eb55a 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap @@ -1,93 +1,95 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render the comment popup correctly when changing a comment 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom-right" -> +<DropdownOverlay> <div - className="issue-comment-form-text" - > - <textarea - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="" - rows="2" - value="*test*" - /> - </div> - <div - className="issue-comment-form-footer" + className="issue-comment-bubble-popup" > <div - className="issue-comment-form-actions" + className="issue-comment-form-text" > - <button - className="js-issue-comment-submit little-spacer-right" - disabled={false} - onClick={[Function]} - > - save - </button> - <a - className="js-issue-comment-cancel" - href="#" - onClick={[Function]} - > - cancel - </a> + <textarea + autoFocus={true} + onChange={[Function]} + onKeyDown={[Function]} + placeholder="" + rows="2" + value="*test*" + /> </div> <div - className="issue-comment-form-tips" + className="issue-comment-form-footer" > - <MarkdownTips /> + <div + className="issue-comment-form-actions" + > + <Button + className="js-issue-comment-submit little-spacer-right" + disabled={false} + onClick={[Function]} + > + save + </Button> + <ResetButtonLink + className="js-issue-comment-cancel" + onClick={[Function]} + > + cancel + </ResetButtonLink> + </div> + <div + className="issue-comment-form-tips" + > + <MarkdownTips /> + </div> </div> </div> -</BubblePopup> +</DropdownOverlay> `; exports[`should render the comment popup correctly without existing comment 1`] = ` -<BubblePopup - customClass="myclass bubble-popup-bottom-right" -> +<DropdownOverlay> <div - className="issue-comment-form-text" - > - <textarea - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="placeholder test" - rows="2" - value="" - /> - </div> - <div - className="issue-comment-form-footer" + className="issue-comment-bubble-popup" > <div - className="issue-comment-form-actions" + className="issue-comment-form-text" > - <button - className="js-issue-comment-submit little-spacer-right" - disabled={true} - onClick={[Function]} - > - issue.comment.submit - </button> - <a - className="js-issue-comment-cancel" - href="#" - onClick={[Function]} - > - cancel - </a> + <textarea + autoFocus={true} + onChange={[Function]} + onKeyDown={[Function]} + placeholder="placeholder test" + rows="2" + value="" + /> </div> <div - className="issue-comment-form-tips" + className="issue-comment-form-footer" > - <MarkdownTips /> + <div + className="issue-comment-form-actions" + > + <Button + className="js-issue-comment-submit little-spacer-right" + disabled={true} + onClick={[Function]} + > + issue.comment.submit + </Button> + <ResetButtonLink + className="js-issue-comment-cancel" + onClick={[Function]} + > + cancel + </ResetButtonLink> + </div> + <div + className="issue-comment-form-tips" + > + <MarkdownTips /> + </div> </div> </div> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap index 57990df2131..a5eb2aafcac 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap @@ -1,22 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render tags popup correctly 1`] = ` -<TagsSelector - listSize={10} - onSearch={[Function]} - onSelect={[Function]} - onUnselect={[Function]} - position={Object {}} - selectedTags={ - Array [ - "mytag", - ] - } - tags={ - Array [ - "test", - "second", - ] - } -/> +<DropdownOverlay + placement="bottom-right" +> + <TagsSelector + listSize={10} + onSearch={[Function]} + onSelect={[Function]} + onUnselect={[Function]} + selectedTags={ + Array [ + "mytag", + ] + } + tags={ + Array [ + "test", + "second", + ] + } + /> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap index f5cc9cfed81..f8818f53cab 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render tags popup correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-menu bubble-popup-bottom" -> +<DropdownOverlay> <SelectList currentItem="MAJOR" items={ @@ -68,5 +66,5 @@ exports[`should render tags popup correctly 1`] = ` severity.INFO </SelectListItem> </SelectList> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap index 2b0be175cbe..a9728b1687a 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render tags popup correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-menu bubble-popup-bottom" -> +<DropdownOverlay> <SelectList currentItem="confirm" items={ @@ -45,5 +43,5 @@ exports[`should render tags popup correctly 1`] = ` issue.transition.wontfix </SelectListItem> </SelectList> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap index 190d91cb61b..5055d807449 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render tags popup correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-menu bubble-popup-bottom" -> +<DropdownOverlay> <SelectList currentItem="BUG" items={ @@ -46,5 +44,5 @@ exports[`should render tags popup correctly 1`] = ` issue.type.CODE_SMELL </SelectListItem> </SelectList> -</BubblePopup> +</DropdownOverlay> `; diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js index ccb6c4d99ad..75ead462050 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js @@ -18,10 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import classNames from 'classnames'; import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent'; -import BubblePopup from '../common/BubblePopup'; import DateFormatter from '../intl/DateFormatter'; +import { Popup, PopupPlacement } from '../ui/popups'; /*:: import type { Metric } from '../types'; */ /*:: import type { Serie } from '../charts/AdvancedTimeline'; */ @@ -47,16 +46,17 @@ export default class PreviewGraphTooltips extends React.PureComponent { const { tooltipIdx } = this.props; const top = 16; let left = this.props.tooltipPos; - let customClass; + let placement = PopupPlacement.RightTop; if (left > this.props.graphWidth - TOOLTIP_WIDTH) { left -= TOOLTIP_WIDTH; - customClass = 'bubble-popup-right'; + placement = PopupPlacement.LeftTop; } return ( - <BubblePopup - customClass={classNames(customClass, 'disabled-pointer-events')} - position={{ top, left, width: TOOLTIP_WIDTH }}> + <Popup + className="overview-analysis-graph-popup disabled-pointer-events" + placement={placement} + style={{ top, left, width: TOOLTIP_WIDTH }}> <div className="overview-analysis-graph-tooltip"> <div className="overview-analysis-graph-tooltip-title"> <DateFormatter date={this.props.selectedDate} long={true} /> @@ -80,7 +80,7 @@ export default class PreviewGraphTooltips extends React.PureComponent { </tbody> </table> </div> - </BubblePopup> + </Popup> ); } } diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap index 95ba6e24426..dc7782ee68b 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap @@ -1,9 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -<BubblePopup - customClass="bubble-popup-right disabled-pointer-events" - position={ +<Popup + className="overview-analysis-graph-popup disabled-pointer-events" + placement="left-top" + style={ Object { "left": -135, "top": 16, @@ -47,5 +48,5 @@ exports[`should render correctly 1`] = ` </tbody> </table> </div> -</BubblePopup> +</Popup> `; diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx index 120f7cdeef5..fb4f5de12dc 100644 --- a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx +++ b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BubblePopup, { BubblePopupPosition } from '../common/BubblePopup'; import MultiSelect from '../common/MultiSelect'; import { translate } from '../../helpers/l10n'; import './TagsList.css'; @@ -28,27 +27,22 @@ interface Props { onSearch: (query: string) => Promise<void>; onSelect: (item: string) => void; onUnselect: (item: string) => void; - position: BubblePopupPosition; selectedTags: string[]; tags: string[]; } export default function TagsSelector(props: Props) { return ( - <BubblePopup - customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300" - position={props.position}> - <MultiSelect - elements={props.tags} - listSize={props.listSize} - onSearch={props.onSearch} - onSelect={props.onSelect} - onUnselect={props.onUnselect} - placeholder={translate('search.search_for_tags')} - selectedElements={props.selectedTags} - validateSearchInput={validateTag} - /> - </BubblePopup> + <MultiSelect + elements={props.tags} + listSize={props.listSize} + onSearch={props.onSearch} + onSelect={props.onSelect} + onUnselect={props.onUnselect} + placeholder={translate('search.search_for_tags')} + selectedElements={props.selectedTags} + validateSearchInput={validateTag} + /> ); } diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap index b6d33603ede..d3a553d60a8 100644 --- a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap @@ -1,61 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render with selected tags 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300" - position={ - Object { - "right": 0, - "top": 0, - } +<MultiSelect + elements={ + Array [ + "foo", + "bar", + "baz", + ] } -> - <MultiSelect - elements={ - Array [ - "foo", - "bar", - "baz", - ] - } - filterSelected={[Function]} - listSize={10} - onSearch={[Function]} - onSelect={[Function]} - onUnselect={[Function]} - placeholder="search.search_for_tags" - renderLabel={[Function]} - selectedElements={ - Array [ - "bar", - ] - } - validateSearchInput={[Function]} - /> -</BubblePopup> + filterSelected={[Function]} + listSize={10} + onSearch={[Function]} + onSelect={[Function]} + onUnselect={[Function]} + placeholder="search.search_for_tags" + renderLabel={[Function]} + selectedElements={ + Array [ + "bar", + ] + } + validateSearchInput={[Function]} +/> `; exports[`should render without tags at all 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300" - position={ - Object { - "right": 0, - "top": 0, - } - } -> - <MultiSelect - elements={Array []} - filterSelected={[Function]} - listSize={10} - onSearch={[Function]} - onSelect={[Function]} - onUnselect={[Function]} - placeholder="search.search_for_tags" - renderLabel={[Function]} - selectedElements={Array []} - validateSearchInput={[Function]} - /> -</BubblePopup> +<MultiSelect + elements={Array []} + filterSelected={[Function]} + listSize={10} + onSearch={[Function]} + onSelect={[Function]} + onUnselect={[Function]} + placeholder="search.search_for_tags" + renderLabel={[Function]} + selectedElements={Array []} + validateSearchInput={[Function]} +/> `; diff --git a/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx index 5a5f7761e15..a78a573f41c 100644 --- a/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx +++ b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx @@ -30,7 +30,7 @@ interface Props { export default function OrganizationListItem({ organization }: Props) { return ( <li> - <OrganizationLink className="dropdown-item-flex" organization={organization}> + <OrganizationLink className="display-flex-center" organization={organization}> <div> <OrganizationAvatar organization={organization} small={true} /> <span className="spacer-left">{organization.name}</span> diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap index bd3a76ad927..9e7c634c9dd 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap @@ -3,7 +3,7 @@ exports[`renders 1`] = ` <li> <OrganizationLink - className="dropdown-item-flex" + className="display-flex-center" organization={ Object { "isAdmin": true, diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap new file mode 100644 index 00000000000..750b6a3782e --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render Popup 1`] = ` +<div + className="popup is-left-top foo" + style={ + Object { + "left": -5, + } + } +> + <PopupArrow + style={ + Object { + "top": -5, + } + } + /> +</div> +`; + +exports[`should render PopupArrow 1`] = ` +<div + className="popup-arrow" + style={ + Object { + "left": -5, + } + } +/> +`; diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx new file mode 100644 index 00000000000..b108eed3601 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Popup, PopupPlacement, PopupArrow } from '../popups'; + +it('should render Popup', () => { + expect( + shallow( + <Popup + arrowStyle={{ top: -5 }} + className="foo" + placement={PopupPlacement.LeftTop} + style={{ left: -5 }} + /> + ) + ).toMatchSnapshot(); +}); + +it('should render PopupArrow', () => { + expect(shallow(<PopupArrow style={{ left: -5 }} />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/ui/popups.css b/server/sonar-web/src/main/js/components/ui/popups.css new file mode 100644 index 00000000000..efd2f6b9838 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/popups.css @@ -0,0 +1,213 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +.popup { + position: absolute; + z-index: var(--popupZIndex); + margin-top: -16px; + margin-left: 8px; + padding: var(--gridSize); + border: 1px solid var(--barBorderColor); + border-radius: 3px; + box-sizing: border-box; + background-color: #ffffff; + box-shadow: var(--defaultShadow); + cursor: default; +} + +.popup.no-padding { + padding: 0; +} + +/* #region .popup-arrow */ +.popup-arrow, +.popup-arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border: 6px solid transparent; +} + +.popup-arrow { + top: 15px; + left: -6px; + border-left-width: 0; + border-right-color: var(--barBorderColor); +} + +.popup-arrow:after { + content: ' '; + left: 1px; + bottom: -6px; + border-left-width: 0; + border-right-color: #ffffff; +} +/* #endregion */ + +/* #region .popup.is-bottom */ +.popup.is-bottom { + top: 100%; + left: 0; + margin: 0; + margin-left: 50%; + transform: translate(-50%, 6px); +} + +.popup.is-bottom .popup-arrow { + top: -6px; + left: calc(50% - 3px); + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: var(--barBorderColor); +} + +.popup.is-bottom .popup-arrow.is-left { + left: 8px; +} + +.popup.is-bottom .popup-arrow:after { + left: -6px; + bottom: -7px; + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: #ffffff; +} +/* #endregion */ + +/* #region .popup.is-bottom-right */ +.popup.is-bottom-right { + top: 100%; + right: 0; + margin: 0; + transform: translateY(6px); +} + +.popup.is-bottom-right .popup-arrow { + top: -6px; + left: auto; + right: 8px; + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: var(--barBorderColor); +} + +.popup.is-bottom-right .popup-arrow:after { + left: -6px; + bottom: -7px; + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: #ffffff; +} +/* #endregion */ + +/* #region .popup.is-bottom-left */ +.popup.is-bottom-left { + top: 100%; + left: 0; + margin: 0; + transform: translateY(6px); +} + +.popup.is-bottom-left .popup-arrow { + top: -6px; + right: auto; + left: 8px; + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: var(--barBorderColor); +} + +.popup.is-bottom-left .popup-arrow:after { + left: -6px; + bottom: -7px; + border-left-width: 6px; + border-top-width: 0; + border-right-color: transparent; + border-bottom-color: #ffffff; +} +/* #endregion */ + +/* #region .popup.is-left-top */ +.popup.is-left-top { + top: -4px; + right: 100%; + margin: 0; + transform: translateX(-6px); +} + +.popup.is-left-top .popup-arrow { + right: -6px; + left: auto; + top: 8px; + border-right-width: 0; + border-left-width: 6px; + border-left-color: var(--barBorderColor); + border-right-color: transparent; +} + +.popup.is-left-top .popup-arrow:after { + top: -6px; + left: -7px; + border-right-width: 0; + border-left-width: 6px; + border-left-color: #ffffff; + border-right-color: transparent; +} +/* #endregion */ + +/* #region .popup.is-right-top */ +.popup.is-right-top { + top: -4px; + left: 100%; + margin: 0; + transform: translateX(6px); +} + +.popup.is-right-top .popup-arrow { + left: -6px; + right: auto; + top: 8px; + border-left-width: 0; + border-right-width: 6px; + border-right-color: var(--barBorderColor); + border-left-color: transparent; +} + +.popup.is-right-top .popup-arrow:after { + top: -6px; + right: -7px; + border-left-width: 0; + border-right-width: 6px; + border-right-color: #ffffff; + border-left-color: transparent; +} +/* #endregion */ + +/* #region .popup & .menu or .multi-select */ +.popup:not(.no-padding) > .menu, +.popup:not(.no-padding) > .multi-select { + margin: calc(-1 * var(--gridSize)); +} +/* #endregion */ diff --git a/server/sonar-web/src/main/js/components/ui/popups.tsx b/server/sonar-web/src/main/js/components/ui/popups.tsx new file mode 100644 index 00000000000..458f7e0b0ae --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/popups.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import './popups.css'; + +export enum PopupPlacement { + Bottom = 'bottom', + BottomLeft = 'bottom-left', + BottomRight = 'bottom-right', + LeftTop = 'left-top', + RightTop = 'right-top' +} + +interface PopupProps { + arrowStyle?: React.CSSProperties; + children?: React.ReactNode; + className?: string; + noPadding?: boolean; + placement?: PopupPlacement; + style?: React.CSSProperties; +} + +export function Popup(props: PopupProps) { + const { placement = PopupPlacement.Bottom } = props; + return ( + <div + className={classNames( + 'popup', + `is-${placement}`, + { 'no-padding': props.noPadding }, + props.className + )} + style={props.style}> + {props.children} + <PopupArrow style={props.arrowStyle} /> + </div> + ); +} + +interface PopupArrowProps { + style?: React.CSSProperties; +} + +export function PopupArrow(props: PopupArrowProps) { + return <div className="popup-arrow" style={props.style} />; +} diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index bdd52ec8e1e..d650182b932 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -78,6 +78,20 @@ export function elementKeydown(element: ShallowWrapper, keyCode: number): void { } } +export function resizeWindowTo(width?: number, height?: number) { + // `document.body.clientWidth/clientHeight` are getters by default, so we need to redefine them + // pass `configurable: true` to allow to redefine the properties multiple times + if (width) { + Object.defineProperty(document.body, 'clientWidth', { configurable: true, value: width }); + } + if (height) { + Object.defineProperty(document.body, 'clientHeight', { configurable: true, value: height }); + } + + const resizeEvent = new Event('resize'); + window.dispatchEvent(resizeEvent); +} + export function doAsync(fn?: Function): Promise<void> { return new Promise(resolve => { setImmediate(() => { |