diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-10-31 16:26:09 +0100 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-11-03 14:28:18 +0100 |
commit | 20a70acea9fad007677c6e5ba28b99a6a7b532cc (patch) | |
tree | 5748fbe5568d81c5f8f0ac72ccbd6cea75d3093e /server/sonar-web/src/main | |
parent | ee2fab297a68aaebcdcb3b71b1fc49f2f89a5d46 (diff) | |
download | sonarqube-20a70acea9fad007677c6e5ba28b99a6a7b532cc.tar.gz sonarqube-20a70acea9fad007677c6e5ba28b99a6a7b532cc.zip |
SONAR-10022 Grouping actions must be consistent
Diffstat (limited to 'server/sonar-web/src/main')
43 files changed, 955 insertions, 1089 deletions
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 e11684d5660..6de1a659782 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 @@ -65,6 +65,11 @@ 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; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js index 22c58b2902a..8c239e9d812 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js @@ -20,6 +20,7 @@ /* @flow */ import React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; +import DeleteIcon from '../../../components/icons-components/DeleteIcon'; import { translate } from '../../../helpers/l10n'; /*:: @@ -65,10 +66,11 @@ export default class Stats extends React.PureComponent { {this.props.isSystemAdmin && ( <Tooltip overlay={translate('background_tasks.cancel_all_tasks')}> <a - className="js-cancel-pending icon-delete spacer-left" + className="js-cancel-pending spacer-left link-no-underline" href="#" - onClick={this.handleCancelAllPending} - /> + onClick={this.handleCancelAllPending}> + <DeleteIcon className="text-text-top" /> + </a> </Tooltip> )} </span> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx index d6510541fc2..d944ebba4ef 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx @@ -23,6 +23,7 @@ import Stacktrace from './Stacktrace'; import { STATUSES } from './../constants'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Task } from '../types'; +import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; interface Props { component?: {}; @@ -42,25 +43,21 @@ export default class TaskActions extends React.PureComponent<Props, State> { stacktraceOpen: false }; - handleFilterClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); + handleFilterClick = () => { this.props.onFilterTask(this.props.task); }; - handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); + handleCancelClick = () => { this.props.onCancelTask(this.props.task); }; - handleShowScannerContextClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); + handleShowScannerContextClick = () => { this.setState({ scannerContextOpen: true }); }; closeScannerContext = () => this.setState({ scannerContextOpen: false }); - handleShowStacktraceClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); + handleShowStacktraceClick = () => { this.setState({ stacktraceOpen: true }); }; @@ -80,55 +77,39 @@ export default class TaskActions extends React.PureComponent<Props, State> { return ( <td className="thin nowrap"> - <div className="dropdown js-task-action"> - <button className="dropdown-toggle" data-toggle="dropdown"> - <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu dropdown-menu-right"> - {canFilter && - task.componentName && ( - <li> - <a className="js-task-filter" href="#" onClick={this.handleFilterClick}> - <i className="spacer-right icon-filter icon-gray" /> - {translateWithParameters( - 'background_tasks.filter_by_component_x', - task.componentName - )} - </a> - </li> - )} - {canCancel && ( - <li> - <a className="js-task-cancel" href="#" onClick={this.handleCancelClick}> - <i className="spacer-right icon-delete" /> - {translate('background_tasks.cancel_task')} - </a> - </li> + <ActionsDropdown className="js-task-action"> + {canFilter && + task.componentName && ( + <ActionsDropdownItem className="js-task-filter" onClick={this.handleFilterClick}> + {translateWithParameters( + 'background_tasks.filter_by_component_x', + task.componentName + )} + </ActionsDropdownItem> )} - {task.hasScannerContext && ( - <li> - <a - className="js-task-show-scanner-context" - href="#" - onClick={this.handleShowScannerContextClick}> - <i className="spacer-right icon-list icon-gray" /> - {translate('background_tasks.show_scanner_context')} - </a> - </li> - )} - {canShowStacktrace && ( - <li> - <a - className="js-task-show-stacktrace" - href="#" - onClick={this.handleShowStacktraceClick}> - <i className="spacer-right icon-list icon-red" /> - {translate('background_tasks.show_stacktrace')} - </a> - </li> - )} - </ul> - </div> + {canCancel && ( + <ActionsDropdownItem + className="js-task-cancel" + destructive={true} + onClick={this.handleCancelClick}> + {translate('background_tasks.cancel_task')} + </ActionsDropdownItem> + )} + {task.hasScannerContext && ( + <ActionsDropdownItem + className="js-task-show-scanner-context" + onClick={this.handleShowScannerContextClick}> + {translate('background_tasks.show_scanner_context')} + </ActionsDropdownItem> + )} + {canShowStacktrace && ( + <ActionsDropdownItem + className="js-task-show-stacktrace" + onClick={this.handleShowStacktraceClick}> + {translate('background_tasks.show_stacktrace')} + </ActionsDropdownItem> + )} + </ActionsDropdown> {this.state.scannerContextOpen && ( <ScannerContext onClose={this.closeScannerContext} task={task} /> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx index ee8225ac722..a5eef35810f 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx @@ -26,6 +26,7 @@ import { getWorkers } from '../../../api/ce'; import { translate } from '../../../helpers/l10n'; import HelpIcon from '../../../components/icons-components/HelpIcon'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import EditIcon from '../../../components/icons-components/EditIcon'; interface State { canSetWorkerCount: boolean; @@ -121,7 +122,12 @@ export default class Workers extends React.PureComponent<{}, State> { {!loading && canSetWorkerCount && ( <Tooltip overlay={translate('background_tasks.change_number_of_workers')}> - <a className="icon-edit spacer-left" href="#" onClick={this.handleChangeClick} /> + <a + className="js-edit link-no-underline spacer-left" + href="#" + onClick={this.handleChangeClick}> + <EditIcon className="text-text-top" /> + </a> </Tooltip> )} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.tsx index 953e79d282a..cc07d0d4510 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.tsx @@ -50,7 +50,7 @@ it('opens form', () => { }); expect(wrapper).toMatchSnapshot(); - click(wrapper.find('.icon-edit')); + click(wrapper.find('.js-edit')); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap index aabe0783b3d..84a4e16e597 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap @@ -4,46 +4,23 @@ exports[`renders 1`] = ` <td className="thin nowrap" > - <div - className="dropdown js-task-action" + <ActionsDropdown + className="js-task-action" > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdownItem + className="js-task-filter" + onClick={[Function]} > - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + background_tasks.filter_by_component_x.foo + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-task-cancel" + destructive={true} + onClick={[Function]} > - <li> - <a - className="js-task-filter" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-filter icon-gray" - /> - background_tasks.filter_by_component_x.foo - </a> - </li> - <li> - <a - className="js-task-cancel" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-delete" - /> - background_tasks.cancel_task - </a> - </li> - </ul> - </div> + background_tasks.cancel_task + </ActionsDropdownItem> + </ActionsDropdown> </td> `; @@ -51,34 +28,16 @@ exports[`renders 2`] = ` <td className="thin nowrap" > - <div - className="dropdown js-task-action" + <ActionsDropdown + className="js-task-action" > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdownItem + className="js-task-filter" + onClick={[Function]} > - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li> - <a - className="js-task-filter" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-filter icon-gray" - /> - background_tasks.filter_by_component_x.foo - </a> - </li> - </ul> - </div> + background_tasks.filter_by_component_x.foo + </ActionsDropdownItem> + </ActionsDropdown> </td> `; @@ -86,58 +45,29 @@ exports[`renders 3`] = ` <td className="thin nowrap" > - <div - className="dropdown js-task-action" + <ActionsDropdown + className="js-task-action" > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdownItem + className="js-task-filter" + onClick={[Function]} + > + background_tasks.filter_by_component_x.foo + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-task-cancel" + destructive={true} + onClick={[Function]} > - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + background_tasks.cancel_task + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-task-show-scanner-context" + onClick={[Function]} > - <li> - <a - className="js-task-filter" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-filter icon-gray" - /> - background_tasks.filter_by_component_x.foo - </a> - </li> - <li> - <a - className="js-task-cancel" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-delete" - /> - background_tasks.cancel_task - </a> - </li> - <li> - <a - className="js-task-show-scanner-context" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-list icon-gray" - /> - background_tasks.show_scanner_context - </a> - </li> - </ul> - </div> + background_tasks.show_scanner_context + </ActionsDropdownItem> + </ActionsDropdown> </td> `; @@ -145,58 +75,29 @@ exports[`renders 4`] = ` <td className="thin nowrap" > - <div - className="dropdown js-task-action" + <ActionsDropdown + className="js-task-action" > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdownItem + className="js-task-filter" + onClick={[Function]} > - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + background_tasks.filter_by_component_x.foo + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-task-cancel" + destructive={true} + onClick={[Function]} > - <li> - <a - className="js-task-filter" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-filter icon-gray" - /> - background_tasks.filter_by_component_x.foo - </a> - </li> - <li> - <a - className="js-task-cancel" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-delete" - /> - background_tasks.cancel_task - </a> - </li> - <li> - <a - className="js-task-show-stacktrace" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-list icon-red" - /> - background_tasks.show_stacktrace - </a> - </li> - </ul> - </div> + background_tasks.cancel_task + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-task-show-stacktrace" + onClick={[Function]} + > + background_tasks.show_stacktrace + </ActionsDropdownItem> + </ActionsDropdown> </td> `; @@ -204,34 +105,17 @@ exports[`renders 5`] = ` <td className="thin nowrap" > - <div - className="dropdown js-task-action" + <ActionsDropdown + className="js-task-action" > - <button - className="dropdown-toggle" - data-toggle="dropdown" - > - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + <ActionsDropdownItem + className="js-task-cancel" + destructive={true} + onClick={[Function]} > - <li> - <a - className="js-task-cancel" - href="#" - onClick={[Function]} - > - <i - className="spacer-right icon-delete" - /> - background_tasks.cancel_task - </a> - </li> - </ul> - </div> + background_tasks.cancel_task + </ActionsDropdownItem> + </ActionsDropdown> </td> `; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.tsx.snap index d9d6b6fe2f2..73abbbf04b4 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.tsx.snap @@ -13,10 +13,14 @@ exports[`opens form 1`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> </div> `; @@ -34,10 +38,14 @@ exports[`opens form 2`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> <WorkersForm onClose={[Function]} @@ -68,10 +76,14 @@ exports[`renders 2`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> </div> `; @@ -97,10 +109,14 @@ exports[`renders 3`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> </div> `; @@ -157,10 +173,14 @@ exports[`updates worker count 1`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> <WorkersForm onClose={[Function]} @@ -190,10 +210,14 @@ exports[`updates worker count 2`] = ` placement="bottom" > <a - className="icon-edit spacer-left" + className="js-edit link-no-underline spacer-left" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </Tooltip> </div> `; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs index 8efd57f4c11..0d40e51c4db 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs @@ -32,6 +32,18 @@ </td> <td class="thin nowrap"> - <a class="js-custom-measure-update icon-edit" title="Update" data-toggle="tooltip" href="#"></a> - <a class="js-custom-measure-delete icon-delete" title="Delete" data-toggle="tooltip" href="#"></a> + <div class="dropdown"> + <button class="dropdown-toggle" data-toggle="dropdown"> + {{settingsIcon}}<i class="icon-dropdown little-spacer-left" /> + </button> + <ul class="dropdown-menu dropdown-menu-right"> + <li> + <a class="js-custom-measure-update" href="#">{{t 'update_verb'}}</a> + </li> + <li class="divider" /> + <li> + <a class="js-custom-measure-delete text-danger" href="#">{{t 'delete'}}</a> + </li> + </ul> + </div> </td> diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs index 9f6bab62321..6d2600c9e41 100644 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs @@ -1,7 +1,19 @@ <div class="pull-right big-spacer-left nowrap"> {{#unless default}} - <a class="js-group-update icon-edit little-spacer-right" title="{{t 'update_details'}}" data-toggle="tooltip" href="#"></a> - <a class="js-group-delete icon-delete" title="{{t 'delete'}}" data-toggle="tooltip" href="#"></a> + <div class="dropdown"> + <button class="dropdown-toggle" data-toggle="dropdown"> + {{settingsIcon}}<i class="icon-dropdown little-spacer-left" /> + </button> + <ul class="dropdown-menu dropdown-menu-right"> + <li> + <a class="js-group-update" href="#">{{t 'update_details'}}</a> + </li> + <li class="divider" /> + <li> + <a class="js-group-delete text-danger" href="#">{{t 'delete'}}</a> + </li> + </ul> + </div> {{/unless}} </div> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs index 4f696e1d491..4033a0c9024 100644 --- a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs @@ -1,6 +1,18 @@ <div class="pull-right big-spacer-left nowrap"> - <a class="js-metric-update icon-edit" title="{{t 'update_verb'}}" data-toggle="tooltip" href="#"></a> - <a class="js-metric-delete icon-delete" title="{{t 'delete'}}" data-toggle="tooltip" href="#"></a> + <div class="dropdown"> + <button class="dropdown-toggle" data-toggle="dropdown"> + {{settingsIcon}}<i class="icon-dropdown little-spacer-left" /> + </button> + <ul class="dropdown-menu dropdown-menu-right"> + <li> + <a class="js-metric-update" href="#">{{t 'update_verb'}}</a> + </li> + <li class="divider" /> + <li> + <a class="js-metric-delete text-danger" href="#">{{t 'delete'}}</a> + </li> + </ul> + </div> </div> <div class="display-inline-block text-top width-30"> 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 7c5c784dfd6..1147d0239be 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 @@ -24,7 +24,9 @@ import { translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import RemoveMemberForm from './forms/RemoveMemberForm'; import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm'; -import SettingsIcon from '../../../components/icons-components/SettingsIcon'; +import ActionsDropdown, { + ActionsDropdownDivider +} from '../../../components/controls/ActionsDropdown'; /*:: import type { Member } from '../../../store/organizationsMembers/actions'; */ /*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */ @@ -64,31 +66,20 @@ export default class MembersListItem extends React.PureComponent { )} {organization.canAdmin && ( <td className="nowrap text-middle text-right"> - <div className="dropdown"> - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown"> - <SettingsIcon style={{ marginTop: 4 }} /> <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <ManageMemberGroupsForm - organizationGroups={this.props.organizationGroups} - organization={this.props.organization} - updateMemberGroups={this.props.updateMemberGroups} - member={this.props.member} - /> - </li> - <li role="separator" className="divider" /> - <li> - <RemoveMemberForm - organization={this.props.organization} - removeMember={this.props.removeMember} - member={this.props.member} - /> - </li> - </ul> - </div> + <ActionsDropdown> + <ManageMemberGroupsForm + organizationGroups={this.props.organizationGroups} + organization={this.props.organization} + updateMemberGroups={this.props.updateMemberGroups} + member={this.props.member} + /> + <ActionsDropdownDivider /> + <RemoveMemberForm + organization={this.props.organization} + removeMember={this.props.removeMember} + member={this.props.member} + /> + </ActionsDropdown> </td> )} </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 0bfde40983a..b88cbc6b739 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 @@ -31,70 +31,41 @@ exports[`should groups at 0 if the groupCount field is not defined (just added u <td className="nowrap text-middle text-right" > - <div - className="dropdown" - > - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown" - > - <SettingsIcon - style={ - Object { - "marginTop": 4, - } + <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", } - /> - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li> - <ManageMemberGroupsForm - member={ - Object { - "avatar": "7daf6c79d4802916d83f6266e24850af", - "login": "john", - "name": "John Doe", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </li> - <li - className="divider" - role="separator" - /> - <li> - <RemoveMemberForm - member={ - Object { - "avatar": "7daf6c79d4802916d83f6266e24850af", - "login": "john", - "name": "John Doe", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </li> - </ul> - </div> + } + /> + </ActionsDropdown> </td> </tr> `; @@ -156,72 +127,43 @@ exports[`should render actions and groups for admin 1`] = ` <td className="nowrap text-middle text-right" > - <div - className="dropdown" - > - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown" - > - <SettingsIcon - style={ - Object { - "marginTop": 4, - } + <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", } - /> - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" - > - <li> - <ManageMemberGroupsForm - member={ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </li> - <li - className="divider" - role="separator" - /> - <li> - <RemoveMemberForm - member={ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - } - } - organization={ - Object { - "canAdmin": true, - "key": "foo", - "name": "Foo", - } - } - /> - </li> - </ul> - </div> + } + /> + </ActionsDropdown> </td> </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 6e8818b6e85..58f6838bc5d 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,6 +24,7 @@ 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'; */ @@ -45,14 +46,22 @@ type State = { */ export default class ManageMemberGroupsForm extends React.PureComponent { + /*:: mounted: boolean */ /*:: props: Props; */ state /*: State */ = { open: false }; - openForm = (evt /*: MouseEvent */) => { - evt.preventDefault(); + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + openForm = () => { this.loadUserGroups(); this.setState({ open: true }); }; @@ -63,9 +72,18 @@ export default class ManageMemberGroupsForm extends React.PureComponent { loadUserGroups = () => { this.setState({ loading: true }); - getUserGroups(this.props.member.login, this.props.organization.key).then(response => { - this.setState({ loading: false, userGroups: keyBy(response.groups, 'name') }); - }); + getUserGroups(this.props.member.login, this.props.organization.key).then( + response => { + if (this.mounted) { + this.setState({ loading: false, userGroups: keyBy(response.groups, 'name') }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); }; isGroupSelected = (groupName /*: string */) => { @@ -148,9 +166,9 @@ export default class ManageMemberGroupsForm extends React.PureComponent { render() { const buttonComponent = ( - <a key="manage-member-button" onClick={this.openForm} href="#"> + <ActionsDropdownItem onClick={this.openForm}> {translate('organization.members.manage_groups')} - </a> + </ActionsDropdownItem> ); if (this.state.open) { return [buttonComponent, this.renderModal()]; 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 94fb72dd24f..9a0db75be63 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,6 +20,7 @@ // @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'; */ @@ -45,8 +46,7 @@ export default class RemoveMemberForm extends React.PureComponent { open: false }; - openForm = (evt /*: MouseEvent */) => { - evt.preventDefault(); + openForm = () => { this.setState({ open: true }); }; @@ -94,9 +94,9 @@ export default class RemoveMemberForm extends React.PureComponent { render() { const buttonComponent = ( - <a key="remove-member-button" onClick={this.openForm} href="#"> + <ActionsDropdownItem destructive={true} onClick={this.openForm}> {translate('organization.members.remove')} - </a> + </ActionsDropdownItem> ); if (this.state.open) { return [buttonComponent, this.renderModal()]; 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 c28b5cbcd9a..63503e40918 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 @@ -82,7 +82,7 @@ it('should render and open the modal', () => { it('should correctly handle user interactions', () => { const form = getMountedForm(); - click(form.wrapper.find('a')); + form.wrapper.find('ActionsDropdownItem').prop('onClick')(); expect(form.wrapper.state('open')).toBeTruthy(); expect(form.instance.loadUserGroups).toBeCalled(); 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 5c007ae4123..0fee4e72b58 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 @@ -42,7 +42,7 @@ it('should correctly handle user interactions', () => { <RemoveMemberForm member={member} removeMember={removeMember} organization={organization} /> ); const instance = wrapper.instance(); - click(wrapper.find('a')); + wrapper.find('ActionsDropdownItem').prop('onClick')(); expect(wrapper.state('open')).toBeTruthy(); instance.handleSubmit(mockEvent); expect(removeMember.mock.calls).toMatchSnapshot(); 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 51d9f708ee7..94ff9bb6705 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 @@ -69,23 +69,20 @@ Object { `; exports[`should render and open the modal 1`] = ` -<a - href="#" - key="manage-member-button" +<ActionsDropdownItem onClick={[Function]} > organization.members.manage_groups -</a> +</ActionsDropdownItem> `; exports[`should render and open the modal 2`] = ` Array [ - <a - href="#" + <ActionsDropdownItem onClick={[Function]} > organization.members.manage_groups - </a>, + </ActionsDropdownItem>, <Modal contentLabel="organization.members.manage_groups" onRequestClose={[Function]} 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 3e362fcf4db..861e8d98f02 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 @@ -14,23 +14,22 @@ Array [ `; exports[`should render and open the modal 1`] = ` -<a - href="#" - key="remove-member-button" +<ActionsDropdownItem + destructive={true} onClick={[Function]} > organization.members.remove -</a> +</ActionsDropdownItem> `; exports[`should render and open the modal 2`] = ` Array [ - <a - href="#" + <ActionsDropdownItem + destructive={true} onClick={[Function]} > organization.members.remove - </a>, + </ActionsDropdownItem>, <Modal contentLabel="users.remove" onRequestClose={[Function]} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js index 1c2e6f0618b..e0a644ff495 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js @@ -23,6 +23,7 @@ import { Link } from 'react-router'; import { difference } from 'lodash'; import Backbone from 'backbone'; import { PermissionTemplateType, CallbackType } from '../propTypes'; +import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; import QualifierIcon from '../../../components/shared/QualifierIcon'; import UpdateView from '../views/UpdateView'; import DeleteView from '../views/DeleteView'; @@ -46,16 +47,14 @@ export default class ActionsCell extends React.PureComponent { router: PropTypes.object }; - handleUpdateClick(e) { - e.preventDefault(); + handleUpdateClick = () => { new UpdateView({ model: new Backbone.Model(this.props.permissionTemplate), refresh: this.props.refresh }).render(); - } + }; - handleDeleteClick(e) { - e.preventDefault(); + handleDeleteClick = () => { new DeleteView({ model: new Backbone.Model(this.props.permissionTemplate) }) @@ -67,14 +66,14 @@ export default class ActionsCell extends React.PureComponent { this.props.refresh(); }) .render(); - } + }; - setDefault(qualifier, e) { - e.preventDefault(); + setDefault = qualifier => () => { setDefaultPermissionTemplate(this.props.permissionTemplate.id, qualifier).then( - this.props.refresh + this.props.refresh, + () => {} ); - } + }; getAvailableQualifiers() { const topQualifiers = @@ -84,16 +83,6 @@ export default class ActionsCell extends React.PureComponent { return difference(topQualifiers, this.props.permissionTemplate.defaultFor); } - renderDropdownIcon(icon) { - const style = { - display: 'inline-block', - width: 16, - marginRight: 4, - textAlign: 'center' - }; - return <div style={style}>{icon}</div>; - } - renderSetDefaultsControl() { const availableQualifiers = this.getAvailableQualifiers(); @@ -108,16 +97,13 @@ export default class ActionsCell extends React.PureComponent { renderSetDefaultLink(qualifier, child) { return ( - <li key={qualifier}> - <a - href="#" - className="js-set-default" - data-qualifier={qualifier} - onClick={this.setDefault.bind(this, qualifier)}> - {this.renderDropdownIcon(<i className="icon-check" />)} - {child} - </a> - </li> + <ActionsDropdownItem + key={qualifier} + className="js-set-default" + data-qualifier={qualifier} + onClick={this.setDefault(qualifier)}> + {child} + </ActionsDropdownItem> ); } @@ -150,40 +136,25 @@ export default class ActionsCell extends React.PureComponent { : '/permission_templates'; return ( - <div className="dropdown"> - <button className="dropdown-toggle" data-toggle="dropdown"> - {translate('actions')} <i className="icon-dropdown" /> - </button> - - <ul className="dropdown-menu dropdown-menu-right"> - {this.renderSetDefaultsControl()} - - {!this.props.fromDetails && ( - <li> - <Link to={{ pathname, query: { id: t.id } }}> - {this.renderDropdownIcon(<i className="icon-edit" />)} - {translate('edit_permissions')} - </Link> - </li> - )} - - <li> - <a href="#" className="js-update" onClick={this.handleUpdateClick.bind(this)}> - {this.renderDropdownIcon(<i className="icon-edit" />)} - {translate('update_details')} - </a> - </li> - - {t.defaultFor.length === 0 && ( - <li> - <a href="#" className="js-delete" onClick={this.handleDeleteClick.bind(this)}> - {this.renderDropdownIcon(<i className="icon-delete" />)} - {translate('delete')} - </a> - </li> - )} - </ul> - </div> + <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> + + {t.defaultFor.length === 0 && ( + <ActionsDropdownItem className="js-delete" onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> + )} + </ActionsDropdown> ); } } 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 517e60cc26f..1f9453b9630 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 @@ -24,7 +24,9 @@ import Events from './Events'; import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; -import SettingsIcon from '../../../components/icons-components/SettingsIcon'; +import ActionsDropdown, { + ActionsDropdownDivider +} from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis } from '../types'; */ @@ -78,44 +80,29 @@ export default class ProjectActivityAnalysis extends React.PureComponent { {(canAddVersion || canAddEvent || canDeleteAnalyses) && ( <div className="project-activity-analysis-actions spacer-left"> - <div className="dropdown display-inline-block"> - <button - className="js-analysis-actions button-small button-compact dropdown-toggle" - data-toggle="dropdown" - onClick={this.stopPropagation}> - <SettingsIcon size={12} style={{ marginTop: 3 }} /> <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu dropdown-menu-right"> - {canAddVersion && ( - <li> - <AddEventForm - addEvent={this.props.addVersion} - analysis={analysis} - addEventButtonText="project_activity.add_version" - /> - </li> - )} - {canAddEvent && ( - <li> - <AddEventForm - addEvent={this.props.addCustomEvent} - analysis={analysis} - addEventButtonText="project_activity.add_custom_event" - /> - </li> - )} - {(canAddVersion || canAddEvent) && - canDeleteAnalyses && <li role="separator" className="divider" />} - {canDeleteAnalyses && ( - <li> - <RemoveAnalysisForm - analysis={analysis} - deleteAnalysis={this.props.deleteAnalysis} - /> - </li> - )} - </ul> - </div> + <ActionsDropdown small={true} toggleClassName="js-analysis-actions"> + {canAddVersion && ( + <AddEventForm + addEvent={this.props.addVersion} + analysis={analysis} + addEventButtonText="project_activity.add_version" + /> + )} + {canAddEvent && ( + <AddEventForm + addEvent={this.props.addCustomEvent} + analysis={analysis} + addEventButtonText="project_activity.add_custom_event" + /> + )} + {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ActionsDropdownDivider />} + {canDeleteAnalyses && ( + <RemoveAnalysisForm + analysis={analysis} + deleteAnalysis={this.props.deleteAnalysis} + /> + )} + </ActionsDropdown> </div> )} 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 520a159d0c0..50ff6ca1680 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,6 +20,7 @@ // @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'; */ @@ -56,12 +57,8 @@ export default class AddEventForm extends React.PureComponent { this.mounted = false; } - openForm = (e /*: Event */) => { - e.preventDefault(); - e.stopPropagation(); - if (this.mounted) { - this.setState({ open: true }); - } + openForm = () => { + this.setState({ open: true }); }; closeForm = () => { @@ -137,9 +134,9 @@ export default class AddEventForm extends React.PureComponent { render() { const linkComponent = ( - <a key="add-event-link" className="js-add-event" href="#" onClick={this.openForm}> + <ActionsDropdownItem className="js-add-event" onClick={this.openForm}> {translate(this.props.addEventButtonText)} - </a> + </ActionsDropdownItem> ); if (this.state.open) { return [linkComponent, this.renderModal()]; 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 b3395d92a72..1203ace800c 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,6 +20,7 @@ // @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'; */ @@ -53,12 +54,8 @@ export default class RemoveAnalysisForm extends React.PureComponent { this.mounted = false; } - openForm = (e /*: Event */) => { - e.preventDefault(); - e.stopPropagation(); - if (this.mounted) { - this.setState({ open: true }); - } + openForm = () => { + this.setState({ open: true }); }; closeForm = () => { @@ -119,9 +116,12 @@ export default class RemoveAnalysisForm extends React.PureComponent { render() { const linkComponent = ( - <a key="delete-analysis-link" className="js-delete-analysis" href="#" onClick={this.openForm}> + <ActionsDropdownItem + className="js-delete-analysis" + destructive={true} + onClick={this.openForm}> {translate('project_activity.delete_analysis')} - </a> + </ActionsDropdownItem> ); if (this.state.open) { return [linkComponent, this.renderModal()]; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx index 5290f17718c..608c98e27b6 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx @@ -28,7 +28,10 @@ import { isShortLivingBranch, isLongLivingBranch } from '../../../helpers/branch import { translate } from '../../../helpers/l10n'; import RenameBranchModal from './RenameBranchModal'; import DateFromNow from '../../../components/intl/DateFromNow'; -import SettingsIcon from '../../../components/icons-components/SettingsIcon'; +import ActionsDropdown, { + ActionsDropdownItem, + ActionsDropdownDivider +} from '../../../components/controls/ActionsDropdown'; interface Props { branch: Branch; @@ -54,9 +57,7 @@ export default class BranchRow extends React.PureComponent<Props, State> { this.mounted = false; } - handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); + handleDeleteClick = () => { this.setState({ deleting: true }); }; @@ -64,9 +65,7 @@ export default class BranchRow extends React.PureComponent<Props, State> { this.setState({ deleting: false }); }; - handleRenameClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); + handleRenameClick = () => { this.setState({ renaming: true }); }; @@ -81,9 +80,7 @@ export default class BranchRow extends React.PureComponent<Props, State> { this.setState({ renaming: false }); }; - handleChangeLeakClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); + handleChangeLeakClick = () => { this.setState({ changingLeak: true }); }; @@ -93,69 +90,32 @@ export default class BranchRow extends React.PureComponent<Props, State> { } }; - render() { + renderActions() { const { branch, component } = this.props; - return ( - <tr> - <td> - <BranchIcon - branch={branch} - className={classNames('little-spacer-right', { - 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan - })} - /> - {branch.name} - {branch.isMain && ( - <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div> + <td className="thin nowrap text-right"> + <ActionsDropdown className="ig-spacer-left"> + {isLongLivingBranch(branch) && ( + <ActionsDropdownItem + className="js-change-leak-period" + onClick={this.handleChangeLeakClick}> + {translate('branches.set_leak_period')} + </ActionsDropdownItem> )} - </td> - <td className="thin nowrap text-right"> - <BranchStatus branch={branch} /> - </td> - <td className="thin nowrap text-right"> - {branch.analysisDate && <DateFromNow date={branch.analysisDate} />} - </td> - <td className="thin nowrap text-right"> - <div className="dropdown big-spacer-left"> - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown"> - <SettingsIcon style={{ marginTop: 4 }} /> <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu dropdown-menu-right"> - {isLongLivingBranch(branch) && ( - <li> - <a - className="js-change-leak-period link-no-underline" - href="#" - onClick={this.handleChangeLeakClick}> - {translate('branches.set_leak_period')} - </a> - </li> - )} - {branch.isMain ? ( - <li> - <a - className="js-rename link-no-underline" - href="#" - onClick={this.handleRenameClick}> - {translate('branches.rename')} - </a> - </li> - ) : ( - <li> - <a - className="js-delete link-no-underline" - href="#" - onClick={this.handleDeleteClick}> - {translate('branches.delete')} - </a> - </li> - )} - </ul> - </div> - </td> + {isLongLivingBranch(branch) && !branch.isMain && <ActionsDropdownDivider />} + {branch.isMain ? ( + <ActionsDropdownItem className="js-rename" onClick={this.handleRenameClick}> + {translate('branches.rename')} + </ActionsDropdownItem> + ) : ( + <ActionsDropdownItem + className="js-delete" + destructive={true} + onClick={this.handleDeleteClick}> + {translate('branches.delete')} + </ActionsDropdownItem> + )} + </ActionsDropdown> {this.state.deleting && ( <DeleteBranchModal @@ -182,6 +142,34 @@ export default class BranchRow extends React.PureComponent<Props, State> { project={component} /> )} + </td> + ); + } + + render() { + const { branch } = this.props; + + return ( + <tr> + <td> + <BranchIcon + branch={branch} + className={classNames('little-spacer-right', { + 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan + })} + /> + {branch.name} + {branch.isMain && ( + <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div> + )} + </td> + <td className="thin nowrap text-right"> + <BranchStatus branch={branch} /> + </td> + <td className="thin nowrap text-right"> + {branch.analysisDate && <DateFromNow date={branch.analysisDate} />} + </td> + {this.renderActions()} </tr> ); } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap index febb99812b8..5a28f352855 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap @@ -37,39 +37,16 @@ exports[`renders main branch 1`] = ` <td className="thin nowrap text-right" > - <div - className="dropdown big-spacer-left" + <ActionsDropdown + className="ig-spacer-left" > - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown" - > - <SettingsIcon - style={ - Object { - "marginTop": 4, - } - } - /> - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + <ActionsDropdownItem + className="js-rename" + onClick={[Function]} > - <li> - <a - className="js-rename link-no-underline" - href="#" - onClick={[Function]} - > - branches.rename - </a> - </li> - </ul> - </div> + branches.rename + </ActionsDropdownItem> + </ActionsDropdown> </td> </tr> `; @@ -116,39 +93,17 @@ exports[`renders short-living branch 1`] = ` <td className="thin nowrap text-right" > - <div - className="dropdown big-spacer-left" + <ActionsDropdown + className="ig-spacer-left" > - <button - className="dropdown-toggle little-spacer-right button-compact" - data-toggle="dropdown" - > - <SettingsIcon - style={ - Object { - "marginTop": 4, - } - } - /> - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + <ActionsDropdownItem + className="js-delete" + destructive={true} + onClick={[Function]} > - <li> - <a - className="js-delete link-no-underline" - href="#" - onClick={[Function]} - > - branches.delete - </a> - </li> - </ul> - </div> + branches.delete + </ActionsDropdownItem> + </ActionsDropdown> </td> </tr> `; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx index f3ea88491a2..5dd7604092d 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import ChangeVisibilityForm from './ChangeVisibilityForm'; import { Organization, Visibility } from '../../app/types'; import { translate } from '../../helpers/l10n'; +import EditIcon from '../../components/icons-components/EditIcon'; export interface Props { hasProvisionPermission?: boolean; @@ -62,10 +63,11 @@ export default class Header extends React.PureComponent<Props, State> { {translate('organization.default_visibility_of_new_projects')}{' '} <strong>{translate('visibility', organization.projectVisibility)}</strong> <a - className="js-change-visibility spacer-left icon-edit" + className="js-change-visibility spacer-left link-no-underline" href="#" - onClick={this.handleChangeVisibilityClick} - /> + onClick={this.handleChangeVisibilityClick}> + <EditIcon className="text-text-top" /> + </a> </span> {this.props.hasProvisionPermission && ( <button id="create-project" onClick={this.handleCreateProjectClick}> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index ce7df2a1c43..2f3693f0630 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -27,6 +27,7 @@ import QualifierIcon from '../../components/shared/QualifierIcon'; import { translate } from '../../helpers/l10n'; import { getComponentPermissionsUrl } from '../../helpers/urls'; import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter'; +import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown'; interface Props { onApplyTemplateClick: (project: Project) => void; @@ -40,9 +41,7 @@ export default class ProjectRow extends React.PureComponent<Props> { this.props.onProjectCheck(this.props.project, checked); }; - handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); + handleApplyTemplateClick = () => { this.props.onApplyTemplateClick(this.props.project); }; @@ -82,23 +81,16 @@ export default class ProjectRow extends React.PureComponent<Props> { </td> <td className="thin nowrap"> - <div className="dropdown"> - <button className="dropdown-toggle" data-toggle="dropdown"> - {translate('actions')} <i className="icon-dropdown" /> - </button> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <Link to={getComponentPermissionsUrl(project.key)}> - {translate('edit_permissions')} - </Link> - </li> - <li> - <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}> - {translate('projects_role.apply_template')} - </a> - </li> - </ul> - </div> + <ActionsDropdown> + <ActionsDropdownItem to={getComponentPermissionsUrl(project.key)}> + {translate('edit_permissions')} + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-apply-template" + onClick={this.handleApplyTemplateClick}> + {translate('projects_role.apply_template')} + </ActionsDropdownItem> + </ActionsDropdown> </td> </tr> ); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap index 9fb16cfbed5..c94a5a5cb29 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap @@ -35,10 +35,14 @@ exports[`renders 1`] = ` visibility.public </strong> <a - className="js-change-visibility spacer-left icon-edit" + className="js-change-visibility spacer-left link-no-underline" href="#" onClick={[Function]} - /> + > + <EditIcon + className="text-text-top" + /> + </a> </span> <button id="create-project" diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap index 87d61db023e..a76855dee44 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -64,49 +64,26 @@ exports[`renders 1`] = ` <td className="thin nowrap" > - <div - className="dropdown" - > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdown> + <ActionsDropdownItem + to={ + Object { + "pathname": "/project_roles", + "query": Object { + "id": "project", + }, + } + } > - actions - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + edit_permissions + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-apply-template" + onClick={[Function]} > - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project_roles", - "query": Object { - "id": "project", - }, - } - } - > - edit_permissions - </Link> - </li> - <li> - <a - className="js-apply-template" - href="#" - onClick={[Function]} - > - projects_role.apply_template - </a> - </li> - </ul> - </div> + projects_role.apply_template + </ActionsDropdownItem> + </ActionsDropdown> </td> </tr> `; @@ -173,49 +150,26 @@ exports[`renders 2`] = ` <td className="thin nowrap" > - <div - className="dropdown" - > - <button - className="dropdown-toggle" - data-toggle="dropdown" + <ActionsDropdown> + <ActionsDropdownItem + to={ + Object { + "pathname": "/project_roles", + "query": Object { + "id": "project", + }, + } + } > - actions - - <i - className="icon-dropdown" - /> - </button> - <ul - className="dropdown-menu dropdown-menu-right" + edit_permissions + </ActionsDropdownItem> + <ActionsDropdownItem + className="js-apply-template" + onClick={[Function]} > - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project_roles", - "query": Object { - "id": "project", - }, - } - } - > - edit_permissions - </Link> - </li> - <li> - <a - className="js-apply-template" - href="#" - onClick={[Function]} - > - projects_role.apply_template - </a> - </li> - </ul> - </div> + projects_role.apply_template + </ActionsDropdownItem> + </ActionsDropdown> </td> </tr> `; 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 8906db09a3b..0a2aff5cffb 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 @@ -19,7 +19,6 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { Link } from 'react-router'; import RenameProfileForm from './RenameProfileForm'; import CopyProfileForm from './CopyProfileForm'; import DeleteProfileForm from './DeleteProfileForm'; @@ -28,8 +27,13 @@ import { getRulesUrl } from '../../../helpers/urls'; import { setDefaultProfile } from '../../../api/quality-profiles'; import { getProfilePath, getProfileComparePath, getProfilesPath } from '../utils'; import { Profile } from '../types'; +import ActionsDropdown, { + ActionsDropdownItem, + ActionsDropdownDivider +} from '../../../components/controls/ActionsDropdown'; interface Props { + className?: string; fromList?: boolean; onRequestFail: (reasong: any) => void; organization: string | null; @@ -57,50 +61,52 @@ export default class ProfileActions extends React.PureComponent<Props, State> { }; } - handleRenameClick = (event: React.SyntheticEvent<HTMLElement>) => { - event.preventDefault(); + handleRenameClick = () => { this.setState({ renameFormOpen: true }); }; handleProfileRename = (name: string) => { this.closeRenameForm(); - this.props.updateProfiles().then(() => { - if (!this.props.fromList) { - this.context.router.replace( - getProfilePath(name, this.props.profile.language, this.props.organization) - ); - } - }); + this.props.updateProfiles().then( + () => { + if (!this.props.fromList) { + this.context.router.replace( + getProfilePath(name, this.props.profile.language, this.props.organization) + ); + } + }, + () => {} + ); }; closeRenameForm = () => { this.setState({ renameFormOpen: false }); }; - handleCopyClick = (event: React.SyntheticEvent<HTMLElement>) => { - event.preventDefault(); + handleCopyClick = () => { this.setState({ copyFormOpen: true }); }; handleProfileCopy = (name: string) => { - this.props.updateProfiles().then(() => { - this.context.router.push( - getProfilePath(name, this.props.profile.language, this.props.organization) - ); - }); + this.props.updateProfiles().then( + () => { + this.context.router.push( + getProfilePath(name, this.props.profile.language, this.props.organization) + ); + }, + () => {} + ); }; closeCopyForm = () => { this.setState({ copyFormOpen: false }); }; - handleSetDefaultClick = (e: React.SyntheticEvent<HTMLElement>) => { - e.preventDefault(); - setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles); + handleSetDefaultClick = () => { + setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles, () => {}); }; - handleDeleteClick = (event: React.SyntheticEvent<HTMLElement>) => { - event.preventDefault(); + handleDeleteClick = () => { this.setState({ deleteFormOpen: true }); }; @@ -131,60 +137,64 @@ export default class ProfileActions extends React.PureComponent<Props, State> { this.props.organization ); + const canActivateRules = actions.edit && !profile.isBuiltIn; + const canRename = actions.edit && !profile.isBuiltIn; + const canSetAsDefault = actions.setAsDefault && !profile.isDefault; + const canDelete = actions.edit && !profile.isDefault && !profile.isBuiltIn; + return ( - <ul className="dropdown-menu dropdown-menu-right"> - {actions.edit && - !profile.isBuiltIn && ( - <li> - <Link to={activateMoreUrl}>{translate('quality_profiles.activate_more_rules')}</Link> - </li> - )} + <ActionsDropdown className={this.props.className}> + {canActivateRules && ( + <ActionsDropdownItem to={activateMoreUrl}> + {translate('quality_profiles.activate_more_rules')} + </ActionsDropdownItem> + )} + {!profile.isBuiltIn && ( - <li> - <a id="quality-profile-backup" href={backupUrl}> - {translate('backup_verb')} - </a> - </li> + <ActionsDropdownItem + download={`${profile.key}.xml`} + id="quality-profile-backup" + to={backupUrl}> + {translate('backup_verb')} + </ActionsDropdownItem> )} - <li> - <Link - to={getProfileComparePath(profile.name, profile.language, this.props.organization)} - id="quality-profile-compare"> - {translate('compare')} - </Link> - </li> + + <ActionsDropdownItem + id="quality-profile-compare" + to={getProfileComparePath(profile.name, profile.language, this.props.organization)}> + {translate('compare')} + </ActionsDropdownItem> + {actions.copy && ( - <li> - <a id="quality-profile-copy" href="#" onClick={this.handleCopyClick}> - {translate('copy')} - </a> - </li> + <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}> + {translate('copy')} + </ActionsDropdownItem> + )} + + {canRename && ( + <ActionsDropdownItem id="quality-profile-rename" onClick={this.handleRenameClick}> + {translate('rename')} + </ActionsDropdownItem> + )} + + {canSetAsDefault && ( + <ActionsDropdownItem + id="quality-profile-set-as-default" + onClick={this.handleSetDefaultClick}> + {translate('set_as_default')} + </ActionsDropdownItem> + )} + + {canDelete && <ActionsDropdownDivider />} + + {canDelete && ( + <ActionsDropdownItem + destructive={true} + id="quality-profile-delete" + onClick={this.handleDeleteClick}> + {translate('delete')} + </ActionsDropdownItem> )} - {actions.edit && - !profile.isBuiltIn && ( - <li> - <a id="quality-profile-rename" href="#" onClick={this.handleRenameClick}> - {translate('rename')} - </a> - </li> - )} - {actions.setAsDefault && - !profile.isDefault && ( - <li> - <a id="quality-profile-set-as-default" href="#" onClick={this.handleSetDefaultClick}> - {translate('set_as_default')} - </a> - </li> - )} - {actions.edit && - !profile.isDefault && - !profile.isBuiltIn && ( - <li> - <a id="quality-profile-delete" href="#" onClick={this.handleDeleteClick}> - {translate('delete')} - </a> - </li> - )} {this.state.copyFormOpen && ( <CopyProfileForm @@ -212,7 +222,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { profile={profile} /> )} - </ul> + </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 dae1d0887fc..ba760e5cb62 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,172 +1,129 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders with all permissions 1`] = ` -<ul - className="dropdown-menu dropdown-menu-right" -> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/org/rules#qprofile=foo|activation=false" - > - quality_profiles.activate_more_rules - </Link> - </li> - <li> - <a - href="/api/qualityprofiles/backup?profileKey=foo" - id="quality-profile-backup" - > - backup_verb - </a> - </li> - <li> - <Link - id="quality-profile-compare" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, - } +<ActionsDropdown> + <ActionsDropdownItem + to="/organizations/org/rules#qprofile=foo|activation=false" + > + 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 - </Link> - </li> - <li> - <a - href="#" - id="quality-profile-copy" - onClick={[Function]} - > - copy - </a> - </li> - <li> - <a - href="#" - id="quality-profile-rename" - onClick={[Function]} - > - rename - </a> - </li> - <li> - <a - href="#" - id="quality-profile-set-as-default" - onClick={[Function]} - > - set_as_default - </a> - </li> - <li> - <a - href="#" - id="quality-profile-delete" - onClick={[Function]} - > - delete - </a> - </li> -</ul> + } + > + 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> `; exports[`renders with no permissions 1`] = ` -<ul - className="dropdown-menu dropdown-menu-right" -> - <li> - <a - href="/api/qualityprofiles/backup?profileKey=foo" - id="quality-profile-backup" - > - backup_verb - </a> - </li> - <li> - <Link - id="quality-profile-compare" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, - } +<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 - </Link> - </li> -</ul> + } + > + compare + </ActionsDropdownItem> +</ActionsDropdown> `; exports[`renders with permission to edit 1`] = ` -<ul - className="dropdown-menu dropdown-menu-right" -> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/organizations/org/rules#qprofile=foo|activation=false" - > - quality_profiles.activate_more_rules - </Link> - </li> - <li> - <a - href="/api/qualityprofiles/backup?profileKey=foo" - id="quality-profile-backup" - > - backup_verb - </a> - </li> - <li> - <Link - id="quality-profile-compare" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/organizations/org/quality_profiles/compare", - "query": Object { - "language": "java", - "name": "Foo", - }, - } +<ActionsDropdown> + <ActionsDropdownItem + to="/organizations/org/rules#qprofile=foo|activation=false" + > + 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 - </Link> - </li> - <li> - <a - href="#" - id="quality-profile-rename" - onClick={[Function]} - > - rename - </a> - </li> - <li> - <a - href="#" - id="quality-profile-delete" - onClick={[Function]} - > - delete - </a> - </li> -</ul> + } + > + compare + </ActionsDropdownItem> + <ActionsDropdownItem + id="quality-profile-rename" + onClick={[Function]} + > + rename + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ActionsDropdownItem + destructive={true} + id="quality-profile-delete" + onClick={[Function]} + > + delete + </ActionsDropdownItem> +</ActionsDropdown> `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx index 5b04d60fdd4..b131bb089de 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx @@ -107,17 +107,13 @@ export default class ProfileHeader extends React.PureComponent<Props> { </Link> </li> <li> - <div className="pull-left dropdown"> - <button className="dropdown-toggle" data-toggle="dropdown"> - {translate('actions')} <i className="icon-dropdown" /> - </button> - <ProfileActions - onRequestFail={this.props.onRequestFail} - organization={organization} - profile={profile} - updateProfiles={this.props.updateProfiles} - /> - </div> + <ProfileActions + className="pull-left" + onRequestFail={this.props.onRequestFail} + organization={organization} + profile={profile} + updateProfiles={this.props.updateProfiles} + /> </li> </ul> </div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx index b838725a953..ff75c1cef3b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx @@ -139,18 +139,13 @@ export default class ProfilesListRow extends React.PureComponent<Props> { {this.renderUsageDate()} </td> <td className="quality-profiles-table-actions thin nowrap text-right"> - <div className="dropdown"> - <button className="dropdown-toggle" data-toggle="dropdown"> - <i className="icon-dropdown" /> - </button> - <ProfileActions - fromList={true} - onRequestFail={this.props.onRequestFail} - organization={this.props.organization} - profile={this.props.profile} - updateProfiles={this.props.updateProfiles} - /> - </div> + <ProfileActions + fromList={true} + onRequestFail={this.props.onRequestFail} + organization={this.props.organization} + profile={this.props.profile} + updateProfiles={this.props.updateProfiles} + /> </td> </tr> ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js index 14b98fceadb..76850ecbf06 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js @@ -21,6 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import PrimitiveInput from './PrimitiveInput'; import { getEmptyValue } from '../../utils'; +import DeleteIcon from '../../../../components/icons-components/DeleteIcon'; export default class MultiValueInput extends React.PureComponent { static propTypes = { @@ -72,7 +73,7 @@ export default class MultiValueInput extends React.PureComponent { <button className="js-remove-value button-clean" onClick={e => this.handleDeleteValue(e, index)}> - <i className="icon-delete" /> + <DeleteIcon /> </button> </div> )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js index 845ca62d952..4016656f336 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js @@ -21,6 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import PrimitiveInput from './PrimitiveInput'; import { getEmptyValue, getUniqueName } from '../../utils'; +import DeleteIcon from '../../../../components/icons-components/DeleteIcon'; export default class PropertySetInput extends React.PureComponent { static propTypes = { @@ -74,7 +75,7 @@ export default class PropertySetInput extends React.PureComponent { <button className="js-remove-value button-link" onClick={e => this.handleDeleteValue(e, index)}> - <i className="icon-delete" /> + <DeleteIcon className="text-middle" /> </button> )} </td> diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js index fc74db42541..6d84a73fc55 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js @@ -20,6 +20,7 @@ // @flow import React from 'react'; import { createProject, deleteProject } from '../../../api/components'; +import DeleteIcon from '../../../components/icons-components/DeleteIcon'; import { translate } from '../../../helpers/l10n'; /*:: @@ -116,7 +117,7 @@ export default class ProjectKeyStep extends React.PureComponent { <i className="spinner" /> ) : ( <button className="button-clean"> - <i className="icon-delete" /> + <DeleteIcon /> </button> )} </form> diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js index 8d36627114f..962b33b1acb 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js @@ -28,6 +28,14 @@ jest.mock('../../../../api/components', () => ({ deleteProject: () => Promise.resolve() })); +jest.mock( + '../../../../components/icons-components/DeleteIcon', + () => + function DeleteIcon() { + return null; + } +); + it('creates new project', async () => { const onDone = jest.fn(); const wrapper = mount(<ProjectKeyStep onDelete={jest.fn()} onDone={onDone} />); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap index cbfbf1da7ca..21ad43db2e6 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap @@ -105,9 +105,7 @@ exports[`creates new project 3`] = ` <button className="button-clean" > - <i - className="icon-delete" - /> + <DeleteIcon /> </button> </form> </div> @@ -138,9 +136,7 @@ exports[`deletes project 1`] = ` <button className="button-clean" > - <i - className="icon-delete" - /> + <DeleteIcon /> </button> </form> </div> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs index 6ff6e8692e1..5ecce84bcdd 100644 --- a/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs +++ b/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs @@ -7,7 +7,7 @@ {{tp 'users.deactivate_user.confirmation' name login}} </div> <div class="modal-foot"> - <button id="deactivate-user-submit">{{t 'users.deactivate'}}</button> + <button class="button-red" id="deactivate-user-submit">{{t 'users.deactivate'}}</button> <a href="#" class="js-modal-close" id="deactivate-user-cancel">{{t 'cancel'}}</a> </div> </form> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs index 63136b96402..5ac0db657c7 100644 --- a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs @@ -63,10 +63,23 @@ </td> <td class="thin nowrap text-right"> - <a class="js-user-update icon-edit little-spacer-right" title="{{t 'update_details'}}" data-toggle="tooltip" href="#"></a> - {{#if local}} - <a class="js-user-change-password icon-lock little-spacer-right" title="{{t 'my_profile.password.title'}}" data-toggle="tooltip" - href="#"></a> - {{/if}} - <a class="js-user-deactivate icon-delete" title="{{t 'users.deactivate'}}" data-toggle="tooltip" href="#"></a> + <div class="dropdown"> + <button class="dropdown-toggle" data-toggle="dropdown"> + {{settingsIcon}}<i class="icon-dropdown little-spacer-left" /> + </button> + <ul class="dropdown-menu dropdown-menu-right"> + <li> + <a class="js-user-update" href="#">{{t 'update_details'}}</a> + </li> + {{#if local}} + <li> + <a class="js-user-change-password" href="#">{{t 'my_profile.password.title'}}</a> + </li> + {{/if}} + <li class="divider" /> + <li> + <a class="js-user-deactivate text-danger" href="#">{{t 'users.deactivate'}}</a> + </li> + </ul> + </div> </td> diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx new file mode 100644 index 00000000000..fbcb30b299c --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -0,0 +1,108 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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 { Link } from 'react-router'; +import { LocationDescriptor } from 'history'; +import SettingsIcon from '../icons-components/SettingsIcon'; + +interface Props { + className?: string; + children: React.ReactNode; + small?: boolean; + toggleClassName?: string; +} + +export default function ActionsDropdown(props: Props) { + return ( + <div className={classNames('dropdown', props.className)}> + <button + className={classNames('dropdown-toggle', props.toggleClassName, { + 'button-small button-compact': props.small + })} + data-toggle="dropdown"> + <SettingsIcon className="text-text-bottom" /> + <i className="icon-dropdown little-spacer-left" /> + </button> + <ul className="dropdown-menu dropdown-menu-right">{props.children}</ul> + </div> + ); +} + +interface ItemProps { + className?: string; + children: React.ReactNode; + destructive?: boolean; + /** used to pass a name of downloaded file */ + download?: string; + id?: string; + onClick?: () => void; + to?: LocationDescriptor; +} + +export class ActionsDropdownItem extends React.PureComponent<ItemProps> { + handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + if (this.props.onClick) { + this.props.onClick(); + } + }; + + render() { + const className = classNames(this.props.className, { 'text-danger': this.props.destructive }); + + if (this.props.download && typeof this.props.to === 'string') { + return ( + <li> + <a + className={className} + download={this.props.download} + href={this.props.to} + id={this.props.id}> + {this.props.children} + </a> + </li> + ); + } + + if (this.props.to) { + return ( + <li> + <Link className={className} id={this.props.id} to={this.props.to}> + {this.props.children} + </Link> + </li> + ); + } + + return ( + <li> + <a className={className} href="#" id={this.props.id} onClick={this.handleClick}> + {this.props.children} + </a> + </li> + ); + } +} + +export function ActionsDropdownDivider() { + return <li className="divider" />; +} 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 f6c73044282..79c2f1c7b18 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 @@ -21,6 +21,8 @@ import React from 'react'; import Avatar from '../../../components/ui/Avatar'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import EditIcon from '../../../components/icons-components/EditIcon'; +import DeleteIcon from '../../../components/icons-components/DeleteIcon'; import CommentDeletePopup from '../popups/CommentDeletePopup'; import CommentPopup from '../popups/CommentPopup'; import DateFromNow from '../../../components/intl/DateFromNow'; @@ -118,9 +120,10 @@ export default class IssueCommentLine extends React.PureComponent { /> }> <button - className="js-issue-comment-edit button-link icon-edit icon-half-transparent" - onClick={this.toggleEditPopup} - /> + className="js-issue-comment-edit button-link icon-half-transparent" + onClick={this.toggleEditPopup}> + <EditIcon /> + </button> </BubblePopupHelper> )} {comment.updatable && ( @@ -132,9 +135,10 @@ export default class IssueCommentLine extends React.PureComponent { togglePopup={this.toggleDeletePopup} popup={<CommentDeletePopup onDelete={this.handleDelete} />}> <button - className="js-issue-comment-delete button-link icon-delete icon-half-transparent" - onClick={this.toggleDeletePopup} - /> + className="js-issue-comment-delete button-link icon-half-transparent little-spacer-left" + onClick={this.toggleDeletePopup}> + <DeleteIcon /> + </button> </BubblePopupHelper> )} </div> 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 d31a9e8eba2..2ffdaa63fd4 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 @@ -80,9 +80,11 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` togglePopup={[Function]} > <button - className="js-issue-comment-edit button-link icon-edit icon-half-transparent" + className="js-issue-comment-edit button-link icon-half-transparent" onClick={[Function]} - /> + > + <EditIcon /> + </button> </BubblePopupHelper> <BubblePopupHelper className="bubble-popup-helper-inline" @@ -102,9 +104,11 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` togglePopup={[Function]} > <button - className="js-issue-comment-delete button-link icon-delete icon-half-transparent" + className="js-issue-comment-delete button-link icon-half-transparent little-spacer-left" onClick={[Function]} - /> + > + <DeleteIcon /> + </button> </BubblePopupHelper> </div> </div> @@ -218,9 +222,11 @@ exports[`should render correctly a comment that is updatable 1`] = ` togglePopup={[Function]} > <button - className="js-issue-comment-edit button-link icon-edit icon-half-transparent" + className="js-issue-comment-edit button-link icon-half-transparent" onClick={[Function]} - /> + > + <EditIcon /> + </button> </BubblePopupHelper> <BubblePopupHelper className="bubble-popup-helper-inline" @@ -240,9 +246,11 @@ exports[`should render correctly a comment that is updatable 1`] = ` togglePopup={[Function]} > <button - className="js-issue-comment-delete button-link icon-delete icon-half-transparent" + className="js-issue-comment-delete button-link icon-half-transparent little-spacer-left" onClick={[Function]} - /> + > + <DeleteIcon /> + </button> </BubblePopupHelper> </div> </div> diff --git a/server/sonar-web/src/main/js/helpers/handlebars/settingsIcon.js b/server/sonar-web/src/main/js/helpers/handlebars/settingsIcon.js new file mode 100644 index 00000000000..48db75bfaeb --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/handlebars/settingsIcon.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +const Handlebars = require('handlebars/runtime'); + +module.exports = function() { + return new Handlebars.default.SafeString(` + <svg class="text-text-bottom" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"> + <g transform="matrix(0.0364583,0,0,0.0364583,0,-1.16667)"> + <path + d="M256,224C256,206.333 249.75,191.25 237.25,178.75C224.75,166.25 209.667,160 192,160C174.333,160 159.25,166.25 146.75,178.75C134.25,191.25 128,206.333 128,224C128,241.667 134.25,256.75 146.75,269.25C159.25,281.75 174.333,288 192,288C209.667,288 224.75,281.75 237.25,269.25C249.75,256.75 256,241.667 256,224ZM384,196.75L384,252.25C384,254.25 383.333,256.167 382,258C380.667,259.833 379,260.917 377,261.25L330.75,268.25C327.583,277.25 324.333,284.833 321,291C326.833,299.333 335.75,310.833 347.75,325.5C349.417,327.5 350.25,329.583 350.25,331.75C350.25,333.917 349.5,335.833 348,337.5C343.5,343.667 335.25,352.667 323.25,364.5C311.25,376.333 303.417,382.25 299.75,382.25C297.75,382.25 295.583,381.5 293.25,380L258.75,353C251.417,356.833 243.833,360 236,362.5C233.333,385.167 230.917,400.667 228.75,409C227.583,413.667 224.583,416 219.75,416L164.25,416C161.917,416 159.875,415.292 158.125,413.875C156.375,412.458 155.417,410.667 155.25,408.5L148.25,362.5C140.083,359.833 132.583,356.75 125.75,353.25L90.5,380C88.833,381.5 86.75,382.25 84.25,382.25C81.917,382.25 79.833,381.333 78,379.5C57,360.5 43.25,346.5 36.75,337.5C35.583,335.833 35,333.917 35,331.75C35,329.75 35.667,327.833 37,326C39.5,322.5 43.75,316.958 49.75,309.375C55.75,301.792 60.25,295.917 63.25,291.75C58.75,283.417 55.333,275.167 53,267L7.25,260.25C5.083,259.917 3.333,258.875 2,257.125C0.667,255.375 0,253.417 0,251.25L0,195.75C0,193.75 0.667,191.833 2,190C3.333,188.167 4.917,187.083 6.75,186.75L53.25,179.75C55.583,172.083 58.833,164.417 63,156.75C56.333,147.25 47.417,135.75 36.25,122.25C34.583,120.25 33.75,118.25 33.75,116.25C33.75,114.583 34.5,112.667 36,110.5C40.333,104.5 48.542,95.542 60.625,83.625C72.708,71.708 80.583,65.75 84.25,65.75C86.417,65.75 88.583,66.583 90.75,68.25L125.25,95C132.583,91.167 140.167,88 148,85.5C150.667,62.833 153.083,47.333 155.25,39C156.417,34.333 159.417,32 164.25,32L219.75,32C222.083,32 224.125,32.708 225.875,34.125C227.625,35.542 228.583,37.333 228.75,39.5L235.75,85.5C243.917,88.167 251.417,91.25 258.25,94.75L293.75,68C295.25,66.5 297.25,65.75 299.75,65.75C301.917,65.75 304,66.583 306,68.25C327.5,88.083 341.25,102.25 347.25,110.75C348.417,112.083 349,113.917 349,116.25C349,118.25 348.333,120.167 347,122C344.5,125.5 340.25,131.042 334.25,138.625C328.25,146.208 323.75,152.083 320.75,156.25C325.083,164.583 328.5,172.75 331,180.75L376.75,187.75C378.917,188.083 380.667,189.125 382,190.875C383.333,192.625 384,194.583 384,196.75Z" + style="fill: currentColor" + /> + </g> + </svg> + `); +}; |