]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10664 Improve dropdown UI/UX consistency (#217)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 9 May 2018 07:17:16 +0000 (09:17 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 9 May 2018 18:20:46 +0000 (20:20 +0200)
240 files changed:
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/BackgroundTaskItem.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SourceViewer.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/issues/Issue.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/organization/MemberItem.java
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.js
server/sonar-web/src/main/js/app/components/search/SearchResults.js
server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
server/sonar-web/src/main/js/app/styles/components/bubble-popup.css [deleted file]
server/sonar-web/src/main/js/app/styles/components/dropdowns.css
server/sonar-web/src/main/js/app/styles/components/issues.css
server/sonar-web/src/main/js/app/styles/components/menu.css
server/sonar-web/src/main/js/app/styles/sonar.css
server/sonar-web/src/main/js/app/styles/style.css
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx
server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap
server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap
server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css [deleted file]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
server/sonar-web/src/main/js/apps/system/components/PageActions.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap
server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/styles.css
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
server/sonar-web/src/main/js/components/common/BubblePopup.tsx [deleted file]
server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
server/sonar-web/src/main/js/components/controls/DateInput.tsx
server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/Dropdown.tsx
server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx
server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/Toggler.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/Tooltip.tsx
server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
server/sonar-web/src/main/js/components/issue/IssueView.js
server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
server/sonar-web/src/main/js/components/issue/components/IssueTags.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
server/sonar-web/src/main/js/components/issue/components/IssueType.js
server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/popups.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/popups.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testUtils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html
tests/src/test/resources/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html

index f4e96d6a47e223d29f96fc79bcda7e99e0d1a8cb..af001cdcf01b8c54f29c27d500e0d52a799bef7a 100644 (file)
@@ -37,7 +37,7 @@ public class BackgroundTaskItem {
 
   public BackgroundTaskItem openActions() {
     elt.$(".js-task-action > .dropdown-toggle").click();
-    elt.$(".js-task-action > .dropdown-menu").shouldBe(Condition.visible);
+    elt.$(".js-task-action .menu").shouldBe(Condition.visible);
     return this;
   }
 
index a61a371ebdef5ee395214904164248d9794d1b7f..cdff3819744f274cdb13ef65bcd5c1e9ccb93ffb 100644 (file)
@@ -33,7 +33,7 @@ public class RuleItem {
 
   public RuleItem filterSimilarRules(String field) {
     elt.$(".js-rule-filter").click();
-    elt.$(".dropdown-menu a[data-field=\"" + field + "\"]").click();
+    elt.$(".menu a[data-field=\"" + field + "\"]").click();
     return this;
   }
 
index 78ed2c8a1469032895fe7c1dc721e162831c794f..b37270ba0149303ebf7da1f59728e3230556f812 100644 (file)
@@ -34,7 +34,7 @@ public class SourceViewer {
 
   public SelenideElement openCoverageDetails(int line) {
     this.el.$(".source-line-coverage[data-line-number=\"" + line + "\"").click();
-    return $(".bubble-popup").shouldBe(visible);
+    return $(".popup").shouldBe(visible);
   }
 
   public SourceViewer shouldHaveNewLines(int ...lines) {
index 997d7975e17889c7732cd046daaeccc1ac7d4cd6..b5256a93499036c05eef8c0c4340fa9e8be43037 100644 (file)
@@ -54,8 +54,8 @@ public class Issue {
   public Issue assigneeSearchResultCount(String query, Integer count) {
     SelenideElement assignLink = elt.find(".js-issue-assign");
     assignLink.click();
-    SelenideElement popupMenu = Selenide.$(".bubble-popup ul.menu").shouldBe(Condition.visible);
-    Selenide.$(".bubble-popup input.search-box-input").shouldBe(Condition.visible).val("").sendKeys(query);
+    SelenideElement popupMenu = Selenide.$(".popup ul.menu").shouldBe(Condition.visible);
+    Selenide.$(".popup input.search-box-input").shouldBe(Condition.visible).val("").sendKeys(query);
     popupMenu.$("li a[data-text='Not assigned']").shouldNot(Condition.exist);
     popupMenu.$$("li").shouldHaveSize(count);
     assignLink.click();
index 8a4979aa4d3073f0587a97317934a26146e04a3c..466620af52b27b44605995aaf2f830a756863f58 100644 (file)
@@ -56,7 +56,7 @@ public class MemberItem {
     tds.shouldHave(CollectionCondition.sizeGreaterThan(3));
     SelenideElement actionTd = tds.get(3);
     actionTd.$("button").should(Condition.exist).click();
-    actionTd.$$(".dropdown-menu > li").get(2).shouldBe(Condition.visible).click();
+    actionTd.$$(".menu > li > a").get(1).shouldBe(Condition.visible).click();
     SelenideElement modal = getModal("Remove user");
     modal.$("button.button-red").shouldBe(Condition.visible).click();
     return this;
@@ -67,7 +67,7 @@ public class MemberItem {
     tds.shouldHave(CollectionCondition.sizeGreaterThan(3));
     SelenideElement actionTd = tds.get(3);
     actionTd.$("button").should(Condition.exist).click();
-    actionTd.$$(".dropdown-menu > li").get(0).shouldBe(Condition.visible).click();
+    actionTd.$$(".menu > li > a").get(0).shouldBe(Condition.visible).click();
     getModal("Manage groups");
     return this;
   }
index 637a95e3fca45fd820ecff9aa7cffff3e3fa2ed6..5280e553a75a43beb4edd556788696705e7f3077 100644 (file)
@@ -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>
     );
   }
 }
index 29b58e4e6e3814c5cd405e4e3e93c0444ee1cb69..20ddca01c150acbb5edb9f6b12b0158ef525e0c9 100644 (file)
@@ -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>
     );
   }
 }
index b1af003796f26c05f9b33a912c61e6ed509e6a85..0984724bfcdb3cec60257546e52cf5ee903f4e0b 100644 (file)
@@ -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>
 `;
index 890e4d5f29eaddee1b13c02a76f755fe503ee11d..a918d734e24959d8684081f6ecbf6884122572fe 100644 (file)
@@ -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>
     );
index 5772988c123c5ace579759a84ddac8d9b55cfdb4..98f8de579cf6e4b5977a823634ecbcec2b44f160 100644 (file)
@@ -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>
     );
   }
 }
index 3a78c92dc46a37231b4c2b233ebb58924cfe61c0..86ea6e3c84a0c5e70db32c3c4a9b57cc9b490124 100644 (file)
@@ -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>
     );
index 8ea67da8b970f12e997a4da33d7d9df290b1e6d2..83a894a3b38dd22dd3b5784d430d40fcfc6ff996 100644 (file)
@@ -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', () => {
index 4ac93c4fb004f991b7b569463060666681495e82..a7b7bb9595313ccfd0b9b753503364cab29960a7 100644 (file)
@@ -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', () => {
index dc23c7b4368f7c588c165b98e7a7634ce21f7c89..3d9fee6a289f75cb7c742dda0f9a69d5c836b4de 100644 (file)
@@ -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"
   >
index cac918b498529475bfee1b0b06a3f21da916e5f3..3296a5ced1832ce1fe9fea9817dddabcbc352f44 100644 (file)
@@ -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>
 `;
index 249f20e49dc28ada6a16dc85cfe476d275b14370..3abe30c25f4ef1ffedea0966a637797d1aa74b08 100644 (file)
@@ -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"
+/>
 `;
index 866edfae8057e795267cd61a4711743d1580c9cf..9e1eabf9be6686b5ebae86befbaf16912067e98b 100644 (file)
@@ -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 && (
index 443f074225cd02a0fdf70fb8688dde746d6e0379..cab30c446aef7d6c47a0360d959564ab42c7de6e 100644 (file)
@@ -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>
     );
index 1ae29cfc8abd3f7ebbc23f9f4e032d1b4a0cfc63..d58fb2379acf1a80e2757410111a8b806fb2f604 100644 (file)
@@ -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>
     );
index b568f713458de27d83c273ba6e8fb20a810f78aa..af85d305412ac23605856233dd3f205e04459f85 100644 (file)
@@ -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>
     );
   }
index 22fa861f34f540c0ebd37b317004aa0a5515d016..ba791499f5904ea9504792f9ded32b2657e35841 100644 (file)
@@ -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', () => {
index 1e02139f691d932f83c89b93a2acc382a518df1d..3e4110f5193bf33d1ea4adc3030f2f37a2bc0772 100644 (file)
@@ -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();
 });
index ec18ecba3a9b45b55e57f5ba02dc7de4aec3b0ab..152efb5d69cdf29b2613392f4cddc7d30c8d307f 100644 (file)
@@ -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();
 });
index 277143865734d045f059602b72d3fe139313ab3e..6eb526f021fc7aa96d9c9a70f7b498354cca4b81 100644 (file)
@@ -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"
+/>
 `;
index 4400a02b25097a2c9d2f1357aae14fe60a829f23..1664c15d58c038cae9ad4526b45ff10e37d75db5 100644 (file)
@@ -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"
+/>
 `;
index e83123dfbdcf7e68fba6d7c8ac8566a988a05ff6..316ff4a8e44a47de71b2e78fcbd0318362e0bd86 100644 (file)
@@ -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>
 `;
index 8379d1822bbe680fb14fa0a2da2f355066ec1ba5..ff656a311a55099cd064bba80c1a489142bececf 100644 (file)
@@ -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>
     );
index ae3f5e900aff2b6dd36be030cb212763fd1b53b6..256662531cb263b7beae2e7ecd16bb3d979be2ed 100644 (file)
@@ -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();
 });
index d7495bbbfc32649fffb829c17d0409cda9e90334..29a88938c4b4cfa030b60121e89bf58d0c4e23e7 100644 (file)
@@ -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"
+  />,
 ]
 `;
index 5912990d2560f6987283d7c8f9b548e9f8c5c0f5..327844759f8093d18f71b1851fe5126288a639a0 100644 (file)
@@ -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
+    );
   }
 }
index c22b619639dccb2a123479342c078305ea165c47..2d8e3b7532f671e33ba944f3313af341e368abc8 100644 (file)
@@ -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>
         );
index 2e300a88cdacbaa49fb379a7c8ddabd44c63e153..01168e7a49bb7460ed78cbc72515ce684108c9c8 100644 (file)
@@ -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);
-});
index 432b5562168ee08ffb1bca53bc49fc92470a1c7f..f13cf142896e59c113d3c7cd9a9184548b332264 100644 (file)
@@ -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 (file)
index 031edf9..0000000
+++ /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;
-}
index d08b64ea06bc385fc3ec5fc19f6f470ed4222335..ef8e2bd3f10fb8ae50e5f2b9ab1959aebda42f2a 100644 (file)
  */
 .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);
   color: var(--secondFontColor);
   font-size: 11px;
 }
-
-.dropdown-item-flex {
-  display: flex !important;
-  justify-content: space-between;
-  align-items: center;
-}
index 3ff1e99190224538d634365e40eb13a3fd1497eb..21eb1a3066a50f111b615349ee2eff2772354024 100644 (file)
 }
 
 .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;
 }
index ac3fd543e5febeb7726d94bcce4b12cc0e36b813..e5bf1c05edab4678c0afa2282e46fa3400a485a8 100644 (file)
   outline: none;
 }
 
+.menu.is-container {
+  padding: 5px;
+}
+
+.menu-item,
 .menu > li > a,
 .menu > li > span {
   display: block;
   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;
 
 .menu-search {
   position: relative;
-  padding: 4px 16px 0;
+  padding: var(--gridSize) calc(2 * var(--gridSize)) 0;
 }
 
 .menu-search .search-box,
   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);
+}
index 9e5a247cdd1d9c1c7e810733bfef1ffa0123ebb5..eaef6aa0909ff035b57a2e311e0861c976d3a797 100644 (file)
@@ -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';
index 4fc6c561948ff61fec14d226b3e1411f92049268..61be8927c26a4ac9510feb285876336d7c2f389c 100644 (file)
 }
 
 .property pre,
-.bubble-popup pre,
 .coding-rules-detail-parameter pre {
   display: inline-block;
   min-width: 100%;
 }
 
 .property blockquote,
-.bubble-popup blockquote,
 .coding-rules-detail-parameter blockquote {
   margin-top: 10px;
   padding: 10px;
index 59808a135ba4ed0723cb1f68139f5460f205500e..e57fc075f6c14e65818552c55ee595664af68abd 100644 (file)
@@ -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'
 };
index 63d039166cca5474ff97edb58cc256808dbf1033..6b47b2d704dde38393f825b1ef68f6f87b47b890 100644 (file)
@@ -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 && (
index 16e9f0d6f6543320e2f5ac455e55815917b620a0..60d51aae6bda1ce6eca6fe23120a3fa400fd2a67 100644 (file)
@@ -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}
index 38d3520623c2423f5fa84a415367583830d7ecd2..72e0c7a8fa0ed61b344f1a4b2d789cc5583af472 100644 (file)
@@ -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}
       />
index 8340279f3adda8e2ef76bfdf11cc14f47a967148..f7106afcc466aced20d958785c0b3403d172103d 100644 (file)
@@ -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 (file)
index f4f2ff1..0000000
+++ /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 (file)
index 0000000..f8da5fa
--- /dev/null
@@ -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 (file)
index 655b02b..0000000
+++ /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 (file)
index 0000000..335d89f
--- /dev/null
@@ -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>
+    );
+  }
+}
index 239b4eeec6c48bae74add6039a0e50936bc9501c..f26474192d166a12e2fba23e0e27106154109b64 100644 (file)
  */
 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 (file)
index 0000000..cde3f66
--- /dev/null
@@ -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__/DeleteButton-test.tsx
deleted file mode 100644 (file)
index 3c032da..0000000
+++ /dev/null
@@ -1,40 +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 DeleteButton from '../DeleteButton';
-
-it('should delete custom measure', () => {
-  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 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');
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx
new file mode 100644 (file)
index 0000000..411e81c
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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 DeleteForm from '../DeleteForm';
+
+it('should render', () => {
+  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'
+  };
+  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__/EditButton-test.tsx
deleted file mode 100644 (file)
index f1efbc3..0000000
+++ /dev/null
@@ -1,50 +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 EditButton from '../EditButton';
-import { click } from '../../../../helpers/testUtils';
-
-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();
-
-  click(wrapper.find('.js-custom-measure-update'));
-  wrapper.update();
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.find('Form').prop<Function>('onSubmit')({
-    ...measure,
-    description: 'new-description',
-    value: 'new-value'
-  });
-  expect(onEdit).toBeCalledWith({ ...measure, description: 'new-description', value: 'new-value' });
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
new file mode 100644 (file)
index 0000000..16942df
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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 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 onEdit = jest.fn();
+  const wrapper = shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={onEdit} />);
+
+  click(wrapper.find('.js-custom-measure-update'));
+  wrapper.update();
+
+  wrapper.find('Form').prop<Function>('onSubmit')({
+    ...measure,
+    description: 'new-description',
+    value: 'new-value'
+  });
+  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 (file)
index c9fc543..0000000
+++ /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 (file)
index 0000000..77d8286
--- /dev/null
@@ -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 (file)
index bd0a803..0000000
+++ /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 (file)
index 0000000..225ca0c
--- /dev/null
@@ -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>
+`;
index 005bedff1e06d126dad158ab8108abefbfe4155c..14d2a0c362d918ef8048ff8dd3b9faa543427208 100644 (file)
@@ -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 (file)
index d825823..0000000
+++ /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 (file)
index 0000000..c799bcb
--- /dev/null
@@ -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 (file)
index d0870fa..0000000
+++ /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 (file)
index 0000000..7f31c64
--- /dev/null
@@ -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>
+    );
+  }
+}
index a5449bf68b6cd4302f1448e6a32daf73c4fb277e..ddbf383ec2d7af185c4811ca2f67b73999a8f238 100644 (file)
  */
 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__/DeleteButton-test.tsx
deleted file mode 100644 (file)
index a35e39a..0000000
+++ /dev/null
@@ -1,32 +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 DeleteButton from '../DeleteButton';
-
-it('should delete metric', () => {
-  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');
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx
new file mode 100644 (file)
index 0000000..bbfe2d0
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 DeleteForm from '../DeleteForm';
+
+it('should render', () => {
+  const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' };
+  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__/EditButton-test.tsx
deleted file mode 100644 (file)
index 5fce344..0000000
+++ /dev/null
@@ -1,49 +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 EditButton from '../EditButton';
-import { click } from '../../../../helpers/testUtils';
-
-it('should edit metric', () => {
-  const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' };
-  const onEdit = jest.fn();
-
-  const wrapper = shallow(
-    <EditButton
-      domains={['Coverage', 'Issues']}
-      metric={metric}
-      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,
-    description: 'bla bla',
-    domain: 'Coverage'
-  });
-  expect(onEdit).toBeCalledWith({ ...metric, description: 'bla bla', domain: 'Coverage' });
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx
new file mode 100644 (file)
index 0000000..dc73103
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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 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 onEdit = jest.fn();
+
+  const wrapper = shallow(
+    <Item
+      domains={['Coverage', 'Issues']}
+      metric={metric}
+      onDelete={jest.fn()}
+      onEdit={onEdit}
+      types={['INT', 'STRING']}
+    />
+  );
+
+  click(wrapper.find('.js-metric-update'));
+  wrapper.update();
+
+  wrapper.find('Form').prop<Function>('onSubmit')({
+    ...metric,
+    description: 'bla bla',
+    domain: 'Coverage'
+  });
+  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 (file)
index bd6e9cd..0000000
+++ /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 (file)
index 0000000..cd0903c
--- /dev/null
@@ -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 (file)
index 66d0785..0000000
+++ /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 (file)
index 0000000..dbef967
--- /dev/null
@@ -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>
+`;
index 1fb174588c7ae2494856c35ff7bbf2feb8954fad..31507c7f042a09619b360c04167563486a856e96 100644 (file)
@@ -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 (file)
index 0000000..a1a3a49
--- /dev/null
@@ -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 (file)
index 2de9ded..0000000
+++ /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}
-          />
-        )}
-      </>
-    );
-  }
-}
index 651066f64b0bc3b52728cb49fdd628e7944e030d..a1f2f8ba6115f434927774e17d284e0073d496ed 100644 (file)
  * 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/apps/groups/components/__tests__/DeleteForm-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx
new file mode 100644 (file)
index 0000000..9aa65bc
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 DeleteForm from '../DeleteForm';
+
+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 (file)
index d650c32..0000000
+++ /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();
-});
index 28c9bb1617a0e85d79bd8f71526242a17588b514..805c151b5cb7f1023ac97c757ac610b67d5a4a3a 100644 (file)
 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 (file)
index 0000000..4d93bc7
--- /dev/null
@@ -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 (file)
index 43bc8b8..0000000
+++ /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>
-`;
index 2146de899dab5e7650c42460151d4def9aa9b91a..59d94b8e22bd66f787e1c98b02fbc763512b2e66 100644 (file)
@@ -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>
index df3a3a1374c5a0471658cb41b62fd45c620412f9..95d0e0daceaeb186477c5d01207c0bc147323783 100644 (file)
@@ -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}>
index 26b333cc334d2fcd165df96cbc881b53b4d26e25..080399bbe71fa0e0af4403504cacc0562b6c2797 100644 (file)
  */
 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>
   );
 }
index b542908b5c28c93ea24ce0d5528eb7024af71d36..a2ff350334be54eba38c032853999cd3d30c49da 100644 (file)
@@ -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>
+  );
 }
index 891502d42e61ef30d2480c57d500c052aaeb2db9..792f218ffacfbdced408d77e1de3ae4104d079fe 100644 (file)
@@ -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>
     );
index b88cbc6b7397c5eb790dcb99e74be175dc5c69fb..2f04b608477984452bba333456848e15381369cb 100644 (file)
@@ -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>
 `;
index 3c748a3588c7f1d55628300f7f10c0433e7a8673..4339538beb12a3131db601fe564c0f888792ec55 100644 (file)
@@ -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;
-  }
 }
index d4dae2fa781fa81045b83b42e95932454ff6db10..343054e6be3d5963ae89cf6347917c43463e5a82 100644 (file)
 // @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;
-  }
 }
index 2bd5905184b79124be506952cf128b1165f50b3a..c2d30153c74b7f2d2f90ced1e9c888d95fa952e2 100644 (file)
@@ -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();
-});
index e02af92237ade62013c28204c72063da91987eea..fca12f0187b543cecd8e6e9b1258542425e1ac8c 100644 (file)
@@ -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'
+  });
 });
index 94ff9bb670542cbdca377a768110436e835ff73c..2eea5b660856193f3f7f744f646882535f2d57bc 100644 (file)
@@ -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>
 `;
index 861e8d98f0216dc65f0121edaf657b3d24834a48..eb235ae0e4ab63cb9ace709bd19a6c25789bd77d 100644 (file)
@@ -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 (file)
index fad5187..0000000
+++ /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);
-}
index 636dc6cf0a15d6f81bebb35bcea85f0bde40bd6c..f9b1e0ddf81af13939ff938de86f7a809fa592ec 100644 (file)
@@ -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 };
index 338494e760424247af94f3f83ad9c7c3da1ff0fc..3b6f30e664f85cc47cb100505b1c7bad4611b38a 100644 (file)
@@ -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>
   );
 }
index 5151194ba9f931ba95d1dda671be6dc8cc9c3b6a..67eac961218078c6dd82b48561ba888f8240e483 100644 (file)
@@ -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>
   );
 }
index c6bc236126a5910b65121e921a45408b7e468ac7..df497ccd8fcd46012e1812a46ab9b38948ab8216 100644 (file)
@@ -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>
index f68fab3ac9f92a5bd7074fff9f581cf4e9909cb8..eb8baeef190802a6e08bac01622810bb8ce26155 100644 (file)
@@ -33,5 +33,5 @@ it('renders', () => {
       }}
     />
   );
-  expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+  expect(wrapper.find('Dropdown')).toMatchSnapshot();
 });
index f6afd014d72dcdd4f7a2c1588f0829e06b9ddc99..57d43dc4682fc635dc73ff5f2ae7ca459b34a2e0 100644 (file)
@@ -52,5 +52,5 @@ it('renders dropdown', () => {
       organizations={organizations}
     />
   );
-  expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+  expect(wrapper.find('Dropdown')).toMatchSnapshot();
 });
index c7764bf1c8b2e6fdcb1f3fde4b36846a5adc9857..27d6f917f784360770e85494d373dfc7028171fe 100644 (file)
@@ -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>
 `;
index 949bb39ff041dba9cdbc5678c2039fcc76c6a009..626ef846617d706bcfe88eb5b9e3e7d516bc3410 100644 (file)
@@ -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>
 `;
index bdc74e4d941452b073f274ba0c8955b7313fad4b..855b3201c0c569d605b7101477b93da063560f8e 100644 (file)
@@ -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 {
index 783688ebf7c47e13aa40987b043fc28538ffb59a..fcc2952d745bac8fa8bca24ffcad1478db180219 100644 (file)
 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}
       />
index 7fa2003a3317a9a9b54c2ae9e93c38f1ec63a5eb..b2ed2b1ac1ecc49c48aae0afbad400ed8ab8d59f 100644 (file)
@@ -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();
-});
index aefeb8d6fb6ddd8f68e948cdc08164ff5dcf2391..0c4c24c9b1b11bee959b205ea362a654ed0488f0 100644 (file)
@@ -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');
index d0b063accf7b2ec52f63152a96db55ce075b2870..7eb0fedde2c2c6870158ccdd1a133506c35882cb 100644 (file)
 // 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>
 `;
 
index 0d0dc6dbee92f4676b704677973a8ec54b5a260c..99b68c862549724e005816efe3d2218dbdc8c48c 100644 (file)
   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;
index 121c008ce9ab646720860ac1d8f7fd56a1c841b3..7ef344a3b23614e5aedba5fdcfc471c41022d779 100644 (file)
@@ -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 (file)
index 0000000..b89b76e
--- /dev/null
@@ -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.js
deleted file mode 100644 (file)
index b4c48de..0000000
+++ /dev/null
@@ -1,68 +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 } from 'enzyme';
-import React from 'react';
-import ActionsCell from '../ActionsCell';
-
-const SAMPLE = {
-  id: 'id',
-  name: 'name',
-  permissions: [],
-  defaultFor: []
-};
-
-function renderActionsCell(props) {
-  return shallow(
-    <ActionsCell
-      permissionTemplate={SAMPLE}
-      topQualifiers={['TRK', 'VW']}
-      refresh={() => true}
-      {...props}
-    />
-  );
-}
-
-it('should set default', () => {
-  const setDefault = renderActionsCell().find('.js-set-default');
-  expect(setDefault.length).toBe(2);
-  expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
-  expect(setDefault.at(1).prop('data-qualifier')).toBe('VW');
-});
-
-it('should not set default', () => {
-  const permissionTemplate = { ...SAMPLE, defaultFor: ['TRK', 'VW'] };
-  const setDefault = renderActionsCell({ permissionTemplate }).find('.js-set-default');
-  expect(setDefault.length).toBe(0);
-});
-
-it('should display all qualifiers for default organization', () => {
-  const organization = { isDefault: true };
-  const setDefault = renderActionsCell({ organization }).find('.js-set-default');
-  expect(setDefault.length).toBe(2);
-  expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
-  expect(setDefault.at(1).prop('data-qualifier')).toBe('VW');
-});
-
-it('should display only projects for custom organization', () => {
-  const organization = { isDefault: false };
-  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/permission-templates/components/__tests__/ActionsCell-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
new file mode 100644 (file)
index 0000000..a3ea017
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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 ActionsCell, { Props } from '../ActionsCell';
+
+const SAMPLE = {
+  createdAt: '2018-01-01',
+  id: 'id',
+  name: 'name',
+  permissions: [],
+  defaultFor: []
+};
+
+function renderActionsCell(props?: Partial<Props>) {
+  return shallow(
+    <ActionsCell
+      permissionTemplate={SAMPLE}
+      refresh={() => true}
+      topQualifiers={['TRK', 'VW']}
+      {...props}
+    />
+  );
+}
+
+it('should set default', () => {
+  const setDefault = renderActionsCell().find('.js-set-default');
+  expect(setDefault.length).toBe(2);
+  expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
+  expect(setDefault.at(1).prop('data-qualifier')).toBe('VW');
+});
+
+it('should not set default', () => {
+  const permissionTemplate = { ...SAMPLE, defaultFor: ['TRK', 'VW'] };
+  const setDefault = renderActionsCell({ permissionTemplate }).find('.js-set-default');
+  expect(setDefault.length).toBe(0);
+});
+
+it('should display all qualifiers for default organization', () => {
+  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');
+  expect(setDefault.at(1).prop('data-qualifier')).toBe('VW');
+});
+
+it('should display only projects for custom organization', () => {
+  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');
+});
index cf17cc3b037269fa00322498ba63de5346737847..ed7b41bb49b53ddbbe5162ab1daae89df9e89863 100644 (file)
  */
 // @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>
     );
   }
 }
index 5324153a4e0b6830a2df6110b61175b8ee7d34f8..d34e8de544858ec748b8c74ad02ea4ef98650ba4 100644 (file)
@@ -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>
         )}
 
index c2723826cfe4893aa6878d80627e7e3ebd96d050..3089427f9acea67c492b54b8cc405c0752d33f02 100644 (file)
@@ -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>
 `;
index b47c72d926b73451861dfb9129d7d70edf63af3b..ded7c2b733c157730127681b1852e3406f4808b6 100644 (file)
@@ -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;
-  }
 }
index 78b47814b1ba0fb4ad39b58969ef814fc6f36760..3bf12702367b2874dcde376dc790791438f33cb9 100644 (file)
@@ -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>
     );
   }
 }
index 81864bf8920dc14c700751b8ecc301aede19dee8..d4bf8bbd2e7dfc02a5d970f51df94e1fd97fea9b 100644 (file)
@@ -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>
   );
 }
index 47125ed09fe6f10e6d73e174030b77e23fbed213..e813985139acd1e6449463cd05ce0bb6fbe8b331 100644 (file)
 // @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;
-  }
 }
index 6298f16e0a3b63bd66c89d16ba7c6734a07b1eee..f0ec4ed6f3cb33bfc66d54404b305630095fff4e 100644 (file)
@@ -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}
index 5c8f4b20cf08cafca1162b9934558161cf472544..2770b226fdcc23ecff7db552434e3d57f6d47257 100644 (file)
@@ -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 {
index daa2f082aae87d3bb0704be7d1f386fc4b5d5afd..935ff76d795267c7b3d9c11e6127985adf5d5e10 100644 (file)
@@ -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>
index a7dede573484f1136af3e84b19712f3e31741902..8cf6f2034964228385c29c39640e6c3c19cd6d65 100644 (file)
@@ -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}
index 4a66fe59a8c27b15d6aa1f10f3c296a7661587b2..438db5c8a777bed2d4a7ca8994b6956ac7f88ffb 100644 (file)
@@ -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>
+      </>
     );
   }
 }
index b30e553d91d0555a52058afac23e3b0acceb8158..df159be98daea3be1a98e3481690c945770d9eda 100644 (file)
@@ -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();
 
index 61ea2fee3de017c74eff23a7942d5b93a00d9f26..cb6c994485715d59e5920b82745f7e3fd0a0ad1d 100644 (file)
@@ -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>
 `;
index 018d35320f7c942f0a80f9f2397e99631fccd531..17dea8322e907e55ee01c726c8b6d7f535b79692 100644 (file)
@@ -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>
+      </>
     );
   }
 }
index 01f3889079065836e2b57016d22656eb743a6f84..86a903e0bef42a9de70b603c7f4744994a30eb7d 100644 (file)
 // 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>
 `;
index 80ef81fbb546f68569008bfad45f3bcf93542d31..8a0750085a4b8274066be987d1a96b87eb823f1e 100644 (file)
  */
 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>
   );
index e34bbf7e10e7d87e229c48977b371b0972c9cd6c..dd4c07d9b84e4b546c4834cddb9a0cb280ffff88 100644 (file)
@@ -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
index 44f9d93a7a827607dcbee9fc633cba687c0022f4..6190c2ed029ed2f2d301ca7cff18c3ec186843c5 100644 (file)
@@ -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', () => {
index 3c698c9ba1900267d64a790f03ffc39629be1768..14a070daf6059ba7f59db893059394a89f1aca90 100644 (file)
@@ -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`] = `
index ccd1471a245a536c182323673ce9b2f6451e9477..5c132436de1d16f8236c402c68750123268fa729 100644 (file)
@@ -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>
index 0fd31e7f6ae2c72ec5fc5e5c1a8b986414bfa596..9b8a3267c76f2b883af950c1ffa968185a28c91e 100644 (file)
@@ -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 (file)
index 0000000..4e17716
--- /dev/null
@@ -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>
+  );
+}
index 0027720ad60f4125a56b7f1941dc3f59e9d7841b..cfbeff267e302ff8771a1709329047ce8079606f 100644 (file)
  */
 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}
+          />
+        )}
       </>
     );
   }
index 70fe2bc5c77eb67e7c0bb2f0fa83adec878a7038..8e1fd5d57f42f9dbc7a79a13c78f9a4b7626407b 100644 (file)
@@ -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);
 });
 
index 7079ff776afb33e45941feb5555494fae8f1b892..f72bc992de3f06ad27fc779e164f66a5f9143c87 100644 (file)
@@ -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>
 `;
index 041856e2426659c510b0b5f030848cf893a112c6..1499a05acb0f9a970259e719251bb368200743c5 100644 (file)
@@ -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">
index 8e5aeca17270cc70b520310d3a455379b58bbd4d..618e258da8b603c495b247d433a422ae972a5965 100644 (file)
@@ -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>
     );
   }
 }
index 163648dd6e24770e5d366e4040eafe89dd343cdc..1322545356fe7e27eb6d578ded600ed28b852e48 100644 (file)
@@ -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>
     );
   }
 }
index 17d98a03ecaa571b522dbc1a8413ac50f7855be8..be4c2d1a8426a08f3b2ef6b8dfea61defe1b5f77 100644 (file)
@@ -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>
       );
     }
index 0121bfa868d45f319034eddf88e9a774d26d52a7..93772dcbb72cf3df032abaa3cd34521a1678d12d 100644 (file)
@@ -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}>
index 9297036d88d33e1ca2011ab464991943375a18d2..15af9ac258dc550baec3245f93281fb01df59fa2 100644 (file)
@@ -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>
     ) : (
index 340304bb280557f423582845d3172fb9336e10a7..34a136d839187d6d422ad7b3b2bde0c28f98626c 100644 (file)
@@ -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>
   );
 }
index e692a3c46f170881b4bdd0b0c38bbf3caf847ef3..83c326a12ea49d056c8d058b0d5c44d6443aa18b 100644 (file)
@@ -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}>
index f89a9aa408b585106c867dfce1bc77baa1d10c99..1e46274b9e97cadf2d9194bdfa371cfdf9ace2a7 100644 (file)
@@ -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>
               </>
index 1a94d48e657bbbbdb993f39f99c172ee09b1e552..fa1b766c12a6fa166cd42a9d2cf54b1863f22ffe 100644 (file)
  */
 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>
   );
 }
index 4e01802b36384ce3d4075d932620105d0ea79d26..e715b7ded52c712ed76e5df6cfdb011b5e40f0ad 100644 (file)
@@ -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>
 `;
 
index 9ea77716f15cf47bd20d20397442d6921f70b9e8..7147eca868e3b72590a646540e13e552b394ad68 100644 (file)
@@ -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>
 `;
 
index 3841048e4aee20d76d642e9d28c2129be42e7194..f5d3c9deff34834ed2d95385c8b0b841695b1aee 100644 (file)
@@ -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>
 `;
index 13d7b6ee859f67b0cbdbea1300f52ccd5f564e96..96b360bb80a5cab701b2da3a8bbed8492d21999b 100644 (file)
@@ -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>
 `;
index c320b6a9b3604030c15a0e353cf3a25fa45c6cd3..dac002460eb90f35bb93bd4f5795bfe6ba83e4c4 100644 (file)
@@ -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>
 `;
index 43ac2de8f8ea2d0932e46cb5ffc297377a5768b5..b053a1bf9af1168ab97bf4ca18fe08e34649a094 100644 (file)
@@ -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
index d846069880209e4c5d0f5a16ac11ceccfa8e108a..99378cb457db117ca5fdfed5b92935491eafba39 100644 (file)
@@ -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>
 `;
index 60a57ab69ba023ee90e4dc20c28c5e9d3ad250c9..839838acfe92944ef6f80c980f284e7bfaa42581 100644 (file)
   clear: both;
 }
 
-.source-viewer-measures .bubble-popup-section {
-  width: 100%;
-}
-
 .source-viewer-measures + .source-viewer-measures {
   margin-top: 40px;
 }
 }
 
 .source-viewer-bubble-popup {
-  top: -16px;
-  left: 100%;
-  width: 480px;
   font-family: var(--baseFontFamily);
   font-size: var(--baseFontSize);
   text-align: left;
index 2723b9dad7ff3e5b3b56b4925332e2ca9ebae272..f4d54a0aa5fc6872e1ced79bab4f6a419f217655 100644 (file)
@@ -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/BubblePopup.tsx b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx
deleted file mode 100644 (file)
index e23a99b..0000000
+++ /dev/null
@@ -1,49 +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';
-
-export interface BubblePopupPosition {
-  top?: number;
-  left?: number;
-  right?: number;
-}
-
-interface Props {
-  customClass?: string;
-  children: React.ReactNode;
-  position?: BubblePopupPosition;
-}
-
-/**
- * Deprecated.
- * Use <Popup /> instead.
- */
-export default function BubblePopup(props: Props) {
-  const popupClass = classNames('bubble-popup', props.customClass);
-  const popupStyle = { ...props.position };
-
-  return (
-    <div className={popupClass} style={popupStyle}>
-      {props.children}
-      <div className="bubble-popup-arrow" />
-    </div>
-  );
-}
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 (file)
index 9f66d02..0000000
+++ /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__/BubblePopup-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js
deleted file mode 100644 (file)
index 750dd8a..0000000
+++ /dev/null
@@ -1,36 +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 } from 'enzyme';
-import React from 'react';
-import BubblePopup from '../BubblePopup';
-
-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();
-});
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 (file)
index ffd00e6..0000000
+++ /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 (file)
index 08a7cb8..0000000
+++ /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 (file)
index 51f8e93..0000000
+++ /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>
-`;
index f1173c88e752c359ad6a3cdbf6e4fcd7be0db28e..252d26ef61818d9349a9e78b51f700efb4b278bd 100644 (file)
@@ -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>
   );
 }
index e5794463a324a776b2c3a5bfa446376a38642c91..886a43211e48e4ceb988a49ac696807ed4b43409 100644 (file)
@@ -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/controls/DocumentClickHandler.tsx b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx
new file mode 100644 (file)
index 0000000..9c13423
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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';
+
+interface Props {
+  children: React.ReactNode;
+  onClick: () => void;
+}
+
+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();
+  };
+
+  render() {
+    return this.props.children;
+  }
+}
index 3b82fd1c408f3b442fa9958beb9befb4e703e654..a90414a235c918dc69090b117b68db8a3eea52be 100644 (file)
  * 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();
+    }
   }
 }
index 3886a89e19f496b095aafbbe53d95f67cafd304d..f5e5d6ec5874b0471600a39a362690b325744db4 100644 (file)
  * 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 (file)
index 0000000..ebf28cf
--- /dev/null
@@ -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 (file)
index 0000000..37bb43b
--- /dev/null
@@ -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;
+}
index ca6803887e9b810ee649edf336e3bca51ec7961c..14dfa11da66ad9682e0f1eb54575da2dc21d8416 100644 (file)
@@ -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>
         )}
       </>
index 37c974480ecb599dd09ae85c38a6c6d875b14a71..564f884d9204f3e5c3ed2bdb0647ab049246e006 100644 (file)
@@ -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]);
 });
index 096dc8356ba667580a9828b2e987f3ca6aa2cca0..52df4675a0b547b7924e93d8b00f8869b3f4d2dd 100644 (file)
  * 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 (file)
index 0000000..9e38932
--- /dev/null
@@ -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 (file)
index 0000000..ee087ec
--- /dev/null
@@ -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>
+  );
+}
index af6836077c75f5858fb6015488d800ee05df16a1..5485b05984e4e6a4c52e967f0b7a2c9dd1fb61e9 100644 (file)
 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 (file)
index 0000000..a782ff8
--- /dev/null
@@ -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>
+`;
index 4a369aceaa4e507a85fcafdfecc92552b8d87285..4127f6e586e4f5086b995976bd9aa5e86f6b3b9d 100644 (file)
@@ -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>
 `;
index 5f814a5347e3a4f5d920f3d4ae46634960ba8ae8..0598c4f9f07e75ded0ec30e4ac48c4f4adfbc507 100644 (file)
@@ -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);
+}
index 3f7d5e84ac7faf41acfd33cd6c57b29b45d51ff1..783fdec65b03e6d409f1f0ec13e5d6c1b86b4544 100644 (file)
  */
 // @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();
index 43f5adc2d919a178bff8c5028c0a1df50db7cc26..a70f6ca5a33584d8a3177e5d403c0a714b91a9c3 100644 (file)
  */
 // @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>
     );
   }
 }
index 01ba1f984a1f11dbc873be258dd3cfb82babd318..814d4053cf47df3f474661c36e84a165aa917d92 100644 (file)
@@ -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>
     );
   }
index 1773bd16a0c376b06e58e9d8ea3a55a30d89c3b2..3a0910f65525710971d4fdf422228e4db49b4c6f 100644 (file)
@@ -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>
index 888d7f2d521c66f6629f441a70fd141de86d9123..1f8a9cb2ae57f1d1ec2cd9427edd4851e5c9be8b 100644 (file)
@@ -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
index 326e64e64ab46529700a53b034e23f97a99b6e74..f75e0e8f3c758b6d0e3467983366256fde26b158 100644 (file)
  */
 // @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} />;
index fb183a7f8b74813ee1885260215d6e9001492433..b9f1fcba0beb7bdb7f85dcac5f15d3ebc4fc06db 100644 (file)
 // @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 (
index 0dad342b1b50b6bc29433da7ed9ae38c36dc9b4a..781eebd1f0622578e078d3e578b029004833a56a 100644 (file)
@@ -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>
index 28379cde1cddb358fc65d4b228a70060eccbb969..3aced6f8685608266aa83549de7af3b062aa4e02 100644 (file)
 // @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}
         />
       );
     }
index 60941288e4b591881d92b42cd408d6384609c5d0..380e8668a329f62f1dfbe2ca56504f35d45f6766 100644 (file)
  */
 // @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 (
index b9da955025a5169a9c938fee24c128ff008e060d..5b900ac2e48c9388659a7441592428f1529f3a48 100644 (file)
  */
 // @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>
     );
   }
 }
index cdd7cfeb6f1f621a6ac20965afb3862fad04c977..ad39643695eb2f206f5cc615dc11a85026486858 100644 (file)
@@ -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();
index 0b2b9071cd82eefc0bef1fa97a851f54c68ead2b..8e03fdef42aec2bab5b5cd8716ae1601cfcdb19e 100644 (file)
@@ -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();
index d54334a61f910a70729f239062657e08c1875f18..2114f36622641a5d3f5ff9e2a0df1dce31619587 100644 (file)
@@ -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();
index a321192729abf3671e726041d80429e9838731c4..90cc0e1d8dc03a41ffc26d907789e9da2e2ee7a7 100644 (file)
@@ -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();
index 98e4cf45c5bfb6de436b1596b21af43576f1b9f3..08b00404347c40c402b0cf2ddd3f448279c2a523 100644 (file)
@@ -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();
index aa353241a8a0fdd73b29b8886083e8e0ca2bd2c8..81d9a512a45b076dee2d93b803f347281a42d14b 100644 (file)
@@ -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();
index 9bfc5d7cd85f5d467c759bfc6d10a265cb09d531..98da66e0449f7b198805bffc5cab510a815b0054 100644 (file)
@@ -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();
index de6dcde42bf79715e5cdf3b49269708e050880e4..51132f338ca280f9043952df89aa889c7ac717a0 100644 (file)
@@ -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();
index d4ff90579c60de7d529e86ebdf3ba96469f1ca65..d768c0c852d51952dc66630d6d38651437d0a2b7 100644 (file)
@@ -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`] = `
index 97f8c58d32acdfc6f76e28fd9e186e39dc70e975..0628de785bf3a75f348db074ffa5703c6ab85dd5 100644 (file)
@@ -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>
 `;
index 7305e5d8472035eb213177c50770c16207f46a4d..3c62307690ecb16e7acc9eec752394609f62016d 100644 (file)
@@ -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>
 `;
index 71f4ef39f22bee92ef4639e265b71a1da759bd4b..4db5ed5aa9c5ef4281eaf7e1ed6d7a54c02bad0c 100644 (file)
@@ -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>
 `;
index 98eb8644b757fa545883c7afb0561c35e6618c86..898a75fd3d669f5ce271ca6df77e8cf50f68e972 100644 (file)
@@ -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`] = `
index fa30aff7c20968a06fd666aaccd578e046e13b72..ca33fb7f1ec4efaee5885c66bfb9fb9252f72355 100644 (file)
@@ -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`] = `
index 30fc9e84c333b708e8f8ed4bde9d59e5f098ab35..8f3694dd8d0e2ba49fa6e72ed7da49d97b6312f5 100644 (file)
@@ -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"
index cd14db53dd2fe4da5e4706b4f1ba093a07125b46..dcfe4ff35ea0e1e49a43781a0adf40114c8c08e8 100644 (file)
@@ -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`] = `
index 77988d1f003af79c3b6056184b7f5545dbe3447f..1eb2844dbf6e5a6cbc5264c4f754dd60114f837b 100644 (file)
@@ -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`] = `
index b3f230810fe6418fb1f50bddadc11fb2285f6517..fcfd515b587f72f93d8aa1addb67dcafc228b274 100644 (file)
@@ -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>
     );
   }
 }
index 9776ab00253ba9ce78ac4d5cde0ba7eb075836d1..c7fd5d89ae8445701b84f0fc7551808587946b9e 100644 (file)
@@ -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>
   );
 }
index 2a17f71383b8f144604a2ea635b805c5c70df94f..bee719a083e52c3031ffa69b46f45c3c5f68acab 100644 (file)
  */
 // @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>
     );
   }
 }
index 270cbe04158b5b5c126c8f34c7e31bb45506e04e..b5658c997d361c496b123b51c43dce463e5007c3 100644 (file)
@@ -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>
     );
   }
 }
index 90b8af5f628fff69d11eb1f8f853200c6b9b2d30..c41c216e8b66a32a25a20bf7f93ab26fa9240f6a 100644 (file)
  */
 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>
     );
   }
 }
index cc03b290475e51c415cf99ba9a7b4bc2dc969bb9..2d3c2232f88c94ae7018b8bf754aa2f5b5d1e6df 100644 (file)
 // @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>
     );
   }
 }
index 44bf942253dc52f7c3df044a2ca190333072dd6f..37d342f7eb2d0662c149f93283bb9089fad4ae8e 100644 (file)
  */
 // @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>
     );
   }
 }
index f623afccd08a3e3f530b1946715028010aa6163e..f69e07cf141b40bd2d574f54ab458172b7490ddf 100644 (file)
 // @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>
     );
   }
 }
index fb9b24780e330cf62102e20204d4be8eea679b48..7a28c6594edb6842905fe8603b897b8db7cc59de 100644 (file)
@@ -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>
     );
   }
 }
index 2256e3fb35a8dcb778df52bd6c605932ddcc2aef..be0c222335d4059d42fd008f22d5554025296ab8 100644 (file)
@@ -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);
 });
index 7716fe669a3c474de05cdd795fd4df6263a6fbb1..6cfd0dcdf649edd5ff8d2f1d38310109f0fb75e8 100644 (file)
@@ -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);
 });
index f66e329ec8794711219017c4c7c25fd732a77aab..00b85fd1817de22f4712f2166dd8371ec8df8f0f 100644 (file)
@@ -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();
index 65cade73cf8ad20ebd52a05fd6e3cd045fa21cdd..f54cfd88b778314d9d052abd044cd7e9149edd96 100644 (file)
@@ -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>
 `;
index 634b35907bec4431daced2c56bf0bb45e07aa63a..43c009289439ec71e3ef8a65a291aaba5dd0a017 100644 (file)
@@ -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>
 `;
index d33898199b78fe9fac939979204ee7637b97d656..8ed5e9eb55a45af4fd771cfcab12c23fd6f324cf 100644 (file)
@@ -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>
 `;
index 57990df2131ccef2196386ad1fb70f4fab2ac173..a5eb2aafcace23d2b085c03d1e8989eb4543f2f7 100644 (file)
@@ -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>
 `;
index f5cc9cfed81e4fe8d042f79f2b66a6314d7c090c..f8818f53cab27e0b63dc6898d3c18cebf18742ff 100644 (file)
@@ -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>
 `;
index 2b0be175cbe85230085f243d19e6b4888076d234..a9728b1687a06606cce0583ac7d4471120a2f4f7 100644 (file)
@@ -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>
 `;
index 190d91cb61b94038b7405cad9bdb1a237a4d1c68..5055d807449201612fbc25d085b82f7323f059ba 100644 (file)
@@ -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>
 `;
index ccb6c4d99adffd1823932932867a26a2ab1644dc..75ead462050bd5ee6f2267f7b4e3bfaeaf7c15ca 100644 (file)
  * 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>
     );
   }
 }
index 95ba6e244266a4f880b9705d19125bb9ae390e82..dc7782ee68bced1289a00de73c9a3caa33e37953 100644 (file)
@@ -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>
 `;
index 120f7cdeef5eaba48837b2f560d508a4f9482346..fb4f5de12dc5e280304d2a6f9bda8f9b7c0f2527 100644 (file)
@@ -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}
+    />
   );
 }
 
index b6d33603ede03dfaff770ab7ad2ef95b1b7194c4..d3a553d60a814441eb9a5f25c9be7d428d42bb57 100644 (file)
@@ -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]}
+/>
 `;
index 5a5f7761e15e4b7218c6866200d0e17d55351180..a78a573f41cb436afb9421950ddbdd579bf8ab9f 100644 (file)
@@ -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>
index bd3a76ad9276f9a3fc655922132ff4c311e9fda4..9e7c634c9dd42c67fa59892173ae197be29d380a 100644 (file)
@@ -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 (file)
index 0000000..750b6a3
--- /dev/null
@@ -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 (file)
index 0000000..b108eed
--- /dev/null
@@ -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 (file)
index 0000000..efd2f6b
--- /dev/null
@@ -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 (file)
index 0000000..458f7e0
--- /dev/null
@@ -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} />;
+}
index bdd52ec8e1ea14e7e8fcd34186ec2f7f88a9c042..d650182b93266a28d60f324d992e1b8ec275f594 100644 (file)
@@ -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(() => {
index ce41b78b1fedc547832b986331ed9508181b8e9f..b29e30924bc153394916b0063f0e325193154723 100644 (file)
@@ -1686,8 +1686,8 @@ metric.major_violations.description=Major issues
 metric.major_violations.name=Major Issues
 metric.minor_violations.description=Minor issues
 metric.minor_violations.name=Minor Issues
-metric.ncloc.description=Non commenting lines of code
 metric.ncloc.name=Lines of Code
+metric.ncloc.description=Non commenting lines of code
 metric.ncloc_language_distribution.description=Non Commenting Lines of Code Distributed By Language
 metric.ncloc_language_distribution.name=Lines of Code Per Language
 metric.new_blocker_violations.description=New Blocker issues
index 37a48368f839b64f8e75393186cf2e67dd7c9dfa..1a3c59b3dd84fe9e3ba2b4e6bd67eac617352141 100644 (file)
   </tr>
   <tr>
     <td>waitForElementPresent</td>
-    <td>css=.bubble-popup</td>
+    <td>css=.popup</td>
     <td></td>
   </tr>
   <tr>
     <td>assertElementPresent</td>
-    <td>css=.bubble-popup .alert</td>
+    <td>css=.popup .alert</td>
     <td></td>
   </tr>
   </tbody>
index 689594fdbb15e6a3bc2e1b4a56dd99cd1889df33..ecb338b7af6e2c26924d7e749c9351a20ca87e20 100644 (file)
   </tr>
   <tr>
     <td>waitForElementPresent</td>
-    <td>css=.bubble-popup</td>
+    <td>css=.popup</td>
     <td></td>
   </tr>
   <tr>
     <td>waitForText</td>
-    <td>css=.bubble-popup</td>
+    <td>css=.popup</td>
     <td>glob:*origin-project*File1.xoo*</td>
   </tr>
   </tbody>