From 09b3d167fa8f399e18a37d56e7c8cbb61f68f97f Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 9 May 2018 09:17:16 +0200 Subject: [PATCH] SONAR-10664 Improve dropdown UI/UX consistency (#217) --- .../util/pageobjects/BackgroundTaskItem.java | 2 +- .../qa/util/pageobjects/RuleItem.java | 2 +- .../qa/util/pageobjects/SourceViewer.java | 2 +- .../qa/util/pageobjects/issues/Issue.java | 4 +- .../pageobjects/organization/MemberItem.java | 4 +- .../embed-docs-modal/EmbedDocsPopup.tsx | 13 +- .../embed-docs-modal/EmbedDocsPopupHelper.tsx | 45 +- .../EmbedDocsPopup-test.tsx.snap | 14 +- .../nav/component/ComponentNavBranch.tsx | 54 +- .../component/ComponentNavBranchesMenu.tsx | 28 +- .../nav/component/ComponentNavMenu.tsx | 52 +- .../__tests__/ComponentNavBranch-test.tsx | 4 +- .../__tests__/ComponentNavMenu-test.tsx | 8 +- .../ComponentNavBranch-test.tsx.snap | 299 +++++-- .../ComponentNavBranchesMenu-test.tsx.snap | 20 +- .../ComponentNavMenu-test.tsx.snap | 751 ++++++++++-------- .../app/components/nav/global/GlobalNav.tsx | 14 +- .../components/nav/global/GlobalNavMenu.tsx | 25 +- .../components/nav/global/GlobalNavPlus.tsx | 47 +- .../components/nav/global/GlobalNavUser.tsx | 85 +- .../global/__tests__/GlobalNavMenu-test.tsx | 2 +- .../global/__tests__/GlobalNavPlus-test.tsx | 10 +- .../global/__tests__/GlobalNavUser-test.tsx | 6 +- .../__snapshots__/GlobalNavMenu-test.tsx.snap | 46 +- .../__snapshots__/GlobalNavPlus-test.tsx.snap | 66 +- .../__snapshots__/GlobalNavUser-test.tsx.snap | 373 ++++----- .../components/nav/settings/SettingsNav.tsx | 205 ++--- .../settings/__tests__/SettingsNav-test.tsx | 2 +- .../__snapshots__/SettingsNav-test.tsx.snap | 384 +++++---- .../main/js/app/components/search/Search.js | 93 ++- .../js/app/components/search/SearchResults.js | 4 +- .../search/__tests__/Search-test.js | 10 - .../__snapshots__/SearchResults-test.js.snap | 10 +- .../js/app/styles/components/bubble-popup.css | 186 ----- .../js/app/styles/components/dropdowns.css | 154 +--- .../main/js/app/styles/components/issues.css | 10 +- .../main/js/app/styles/components/menu.css | 24 +- .../src/main/js/app/styles/sonar.css | 1 - .../src/main/js/app/styles/style.css | 2 - server/sonar-web/src/main/js/app/theme.js | 3 +- .../coding-rules/components/BulkChange.tsx | 74 +- .../components/RuleDetailsMeta.tsx | 31 +- .../components/RuleDetailsTagsPopup.tsx | 3 - .../components/SimilarRulesFilter.tsx | 108 +-- .../components/DeleteButton.tsx | 53 -- .../custom-measures/components/DeleteForm.tsx | 64 ++ .../custom-measures/components/EditButton.tsx | 79 -- .../apps/custom-measures/components/Item.tsx | 158 ++++ .../apps/custom-measures/components/List.tsx | 72 +- .../components/MeasureDate.tsx | 51 ++ ...eteButton-test.tsx => DeleteForm-test.tsx} | 13 +- .../{EditButton-test.tsx => Item-test.tsx} | 43 +- .../__snapshots__/DeleteButton-test.tsx.snap | 12 - .../__snapshots__/DeleteForm-test.tsx.snap | 46 ++ .../__snapshots__/EditButton-test.tsx.snap | 48 -- .../__snapshots__/Item-test.tsx.snap | 89 +++ .../__snapshots__/List-test.tsx.snap | 272 ++----- .../components/DeleteButton.tsx | 47 -- .../custom-metrics/components/DeleteForm.tsx | 61 ++ .../custom-metrics/components/EditButton.tsx | 83 -- .../apps/custom-metrics/components/Item.tsx | 150 ++++ .../apps/custom-metrics/components/List.tsx | 50 +- ...eteButton-test.tsx => DeleteForm-test.tsx} | 13 +- .../{EditButton-test.tsx => Item-test.tsx} | 27 +- .../__snapshots__/DeleteButton-test.tsx.snap | 12 - .../__snapshots__/DeleteForm-test.tsx.snap | 46 ++ .../__snapshots__/EditButton-test.tsx.snap | 49 -- .../__snapshots__/Item-test.tsx.snap | 61 ++ .../__snapshots__/List-test.tsx.snap | 150 +--- .../js/apps/groups/components/DeleteForm.tsx | 61 ++ .../js/apps/groups/components/EditGroup.tsx | 84 -- .../js/apps/groups/components/ListItem.tsx | 106 ++- .../components/__tests__/DeleteForm-test.tsx} | 21 +- .../components/__tests__/EditGroup-test.tsx | 58 -- .../components/__tests__/ListItem-test.tsx | 26 +- .../__snapshots__/DeleteForm-test.tsx.snap | 46 ++ .../__snapshots__/EditGroup-test.tsx.snap | 32 - .../__snapshots__/ListItem-test.tsx.snap | 30 +- .../main/js/apps/issues/components/App.tsx | 42 +- .../components/PluginChangeLog.tsx | 40 +- .../components/PluginChangeLogButton.tsx | 45 +- .../components/MembersListItem.js | 72 +- .../MembersListItem-test.js.snap | 120 +-- .../forms/ManageMemberGroupsForm.js | 42 +- .../components/forms/RemoveMemberForm.js | 57 +- .../__tests__/ManageMemberGroupsForm-test.js | 33 +- .../forms/__tests__/RemoveMemberForm-test.js | 29 +- .../ManageMemberGroupsForm-test.js.snap | 178 +---- .../RemoveMemberForm-test.js.snap | 103 +-- .../navigation/OrganizationNavigation.css | 57 -- .../navigation/OrganizationNavigation.tsx | 1 - .../OrganizationNavigationAdministration.tsx | 116 ++- .../OrganizationNavigationExtensions.tsx | 48 +- .../OrganizationNavigationHeader.tsx | 28 +- ...anizationNavigationAdministration-test.tsx | 2 +- .../OrganizationNavigationHeader-test.tsx | 2 +- ...tionNavigationAdministration-test.tsx.snap | 137 ++-- ...OrganizationNavigationHeader-test.tsx.snap | 61 +- .../main/js/apps/overview/meta/MetaTags.tsx | 70 +- .../apps/overview/meta/MetaTagsSelector.tsx | 3 - .../overview/meta/__tests__/MetaTags-test.tsx | 19 - .../meta/__tests__/MetaTagsSelector-test.tsx | 16 +- .../__snapshots__/MetaTags-test.tsx.snap | 134 +--- .../src/main/js/apps/overview/styles.css | 6 +- .../components/ActionsCell.tsx | 80 +- .../components/DeleteForm.tsx | 64 ++ ...tionsCell-test.js => ActionsCell-test.tsx} | 13 +- .../components/GraphsTooltips.js | 16 +- .../components/ProjectActivityAnalysis.js | 104 ++- .../__snapshots__/GraphsTooltips-test.js.snap | 27 +- .../components/forms/AddEventForm.js | 46 +- .../components/forms/AddGraphMetric.js | 64 +- .../components/forms/AddGraphMetricPopup.tsx | 7 +- .../components/forms/RemoveAnalysisForm.js | 47 +- .../apps/projectBranches/components/App.tsx | 20 +- .../__tests__/__snapshots__/App-test.tsx.snap | 26 +- .../components/NoFavoriteProjects.tsx | 33 +- .../NoFavoriteProjects-test.tsx.snap | 40 +- .../projectsManagement/ProjectRowActions.tsx | 36 +- .../__tests__/ProjectRowActions-test.tsx | 2 +- .../ProjectRowActions-test.tsx.snap | 84 +- .../components/ProfileActions.tsx | 100 +-- .../ProfileActions-test.tsx.snap | 254 +++--- .../home/ProfilesListHeader.tsx | 53 +- .../js/apps/system/components/PageActions.tsx | 94 ++- .../components/__tests__/PageActions-test.tsx | 2 +- .../__snapshots__/PageActions-test.tsx.snap | 157 ++-- .../js/apps/users/components/UserActions.tsx | 2 +- .../__snapshots__/UserActions-test.tsx.snap | 4 +- .../webhooks/components/DeleteWebhookForm.tsx | 61 ++ .../webhooks/components/WebhookActions.tsx | 48 +- .../__tests__/WebhookActions-test.tsx | 11 +- .../WebhookActions-test.tsx.snap | 28 +- .../SourceViewer/SourceViewerHeader.tsx | 90 +-- .../SourceViewer/components/CoveragePopup.tsx | 96 +-- .../components/DuplicationPopup.tsx | 15 +- .../SourceViewer/components/LineCoverage.tsx | 21 +- .../components/LineDuplicationBlock.tsx | 23 +- .../SourceViewer/components/LineNumber.tsx | 16 +- .../components/LineOptionsPopup.tsx | 12 +- .../SourceViewer/components/LineSCM.tsx | 20 +- .../MeasuresOverlayCoveredFiles.tsx | 8 +- .../SourceViewer/components/SCMPopup.tsx | 26 +- .../__snapshots__/LineCoverage-test.tsx.snap | 28 +- .../LineDuplicationBlock-test.tsx.snap | 24 +- .../__snapshots__/LineNumber-test.tsx.snap | 15 +- .../LineOptionsPopup-test.tsx.snap | 8 +- .../__snapshots__/LineSCM-test.tsx.snap | 67 +- .../MeasuresOverlayCoveredFiles-test.tsx.snap | 14 +- .../__snapshots__/SCMPopup-test.tsx.snap | 26 +- .../js/components/SourceViewer/styles.css | 7 - .../js/components/charts/AdvancedTimeline.js | 25 +- .../components/common/BubblePopupHelper.tsx | 111 --- .../__tests__/BubblePopupHelper-test.js | 143 ---- .../__snapshots__/BubblePopup-test.js.snap | 20 - .../BubblePopupHelper-test.js.snap | 182 ----- .../components/controls/ActionsDropdown.tsx | 34 +- .../main/js/components/controls/DateInput.tsx | 132 ++- .../DocumentClickHandler.tsx} | 48 +- .../main/js/components/controls/Dropdown.tsx | 147 +++- .../controls/OutsideClickHandler.tsx | 17 +- .../controls/ScreenPositionFixer.tsx | 108 +++ .../main/js/components/controls/Toggler.tsx | 102 +++ .../main/js/components/controls/Tooltip.tsx | 79 +- .../controls/__tests__/DateInput-test.tsx | 12 +- .../controls/__tests__/Dropdown-test.tsx | 100 ++- .../__tests__/ScreenPositionFixer-test.tsx | 98 +++ .../controls/__tests__/Toggler-test.tsx | 71 ++ .../__snapshots__/DateInput-test.tsx.snap | 512 ++++++------ .../__snapshots__/Toggler-test.tsx.snap | 50 ++ .../__snapshots__/Tooltip-test.tsx.snap | 38 +- .../src/main/js/components/issue/IssueView.js | 20 +- .../issue/components/IssueAssign.js | 48 +- .../issue/components/IssueChangelog.js | 51 +- .../issue/components/IssueCommentAction.js | 31 +- .../issue/components/IssueCommentLine.js | 84 +- .../issue/components/IssueMessage.js | 1 - .../issue/components/IssueSeverity.js | 41 +- .../components/issue/components/IssueTags.js | 52 +- .../issue/components/IssueTitleBar.js | 10 +- .../issue/components/IssueTransition.js | 49 +- .../components/issue/components/IssueType.js | 40 +- .../issue/components/SimilarIssuesFilter.js | 34 +- .../components/__tests__/IssueAssign-test.js | 8 +- .../__tests__/IssueChangelog-test.js | 2 +- .../__tests__/IssueCommentAction-test.js | 6 +- .../__tests__/IssueCommentLine-test.js | 4 +- .../__tests__/IssueSeverity-test.js | 2 +- .../components/__tests__/IssueTags-test.js | 2 +- .../__tests__/IssueTransition-test.js | 2 +- .../components/__tests__/IssueType-test.js | 2 +- .../__snapshots__/IssueAssign-test.js.snap | 183 +++-- .../__snapshots__/IssueChangelog-test.js.snap | 142 ++-- .../IssueCommentAction-test.js.snap | 42 +- .../IssueCommentLine-test.js.snap | 203 +++-- .../__snapshots__/IssueSeverity-test.js.snap | 121 ++- .../__snapshots__/IssueTags-test.js.snap | 137 ++-- .../__snapshots__/IssueTitleBar-test.js.snap | 2 - .../IssueTransition-test.js.snap | 190 ++--- .../__snapshots__/IssueType-test.js.snap | 125 ++- .../components/issue/popups/ChangelogPopup.js | 10 +- .../issue/popups/CommentDeletePopup.js | 13 +- .../components/issue/popups/CommentPopup.js | 64 +- .../issue/popups/SetAssigneePopup.js | 10 +- .../issue/popups/SetIssueTagsPopup.tsx | 23 +- .../issue/popups/SetSeverityPopup.js | 13 +- .../issue/popups/SetTransitionPopup.js | 13 +- .../components/issue/popups/SetTypePopup.js | 13 +- .../issue/popups/SimilarIssuesPopup.js | 11 +- .../__tests__/CommentDeletePopup-test.js | 2 +- .../popups/__tests__/CommentPopup-test.js | 18 +- .../__tests__/SetIssueTagsPopup-test.tsx | 7 +- .../__snapshots__/ChangelogPopup-test.js.snap | 8 +- .../CommentDeletePopup-test.js.snap | 12 +- .../__snapshots__/CommentPopup-test.js.snap | 138 ++-- .../SetIssueTagsPopup-test.tsx.snap | 39 +- .../SetSeverityPopup-test.js.snap | 6 +- .../SetTransitionPopup-test.js.snap | 6 +- .../__snapshots__/SetTypePopup-test.js.snap | 6 +- .../preview-graph/PreviewGraphTooltips.js | 16 +- .../PreviewGraphTooltips-test.js.snap | 9 +- .../main/js/components/tags/TagsSelector.tsx | 26 +- .../__snapshots__/TagsSelector-test.tsx.snap | 86 +- .../js/components/ui/OrganizationListItem.tsx | 2 +- .../OrganizationListItem-test.tsx.snap | 2 +- .../__snapshots__/popups-test.tsx.snap | 31 + .../components/ui/__tests__/popups-test.tsx | 39 + .../src/main/js/components/ui/popups.css | 213 +++++ .../src/main/js/components/ui/popups.tsx | 64 ++ .../src/main/js/helpers/testUtils.ts | 14 + .../resources/org/sonar/l10n/core.properties | 2 +- .../duplications-with-deleted-project.html | 4 +- .../cross-project-duplications-viewer.html | 4 +- 233 files changed, 6800 insertions(+), 6780 deletions(-) delete mode 100644 server/sonar-web/src/main/js/app/styles/components/bubble-popup.css delete mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx create mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx create mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx create mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx rename server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/{DeleteButton-test.tsx => DeleteForm-test.tsx} (77%) rename server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/{EditButton-test.tsx => Item-test.tsx} (60%) delete mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx create mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx create mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx rename server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/{DeleteButton-test.tsx => DeleteForm-test.tsx} (74%) rename server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/{EditButton-test.tsx => Item-test.tsx} (70%) delete mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx rename server/sonar-web/src/main/js/{components/common/__tests__/BubblePopup-test.js => apps/groups/components/__tests__/DeleteForm-test.tsx} (73%) delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css create mode 100644 server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx rename server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/{ActionsCell-test.js => ActionsCell-test.tsx} (88%) create mode 100644 server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx delete mode 100644 server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx delete mode 100644 server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js delete mode 100644 server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap delete mode 100644 server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap rename server/sonar-web/src/main/js/components/{common/BubblePopup.tsx => controls/DocumentClickHandler.tsx} (61%) create mode 100644 server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/Toggler.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx create mode 100644 server/sonar-web/src/main/js/components/ui/popups.css create mode 100644 server/sonar-web/src/main/js/components/ui/popups.tsx diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/BackgroundTaskItem.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/BackgroundTaskItem.java index f4e96d6a47e..af001cdcf01 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/BackgroundTaskItem.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/BackgroundTaskItem.java @@ -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; } diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java index a61a371ebde..cdff3819744 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java @@ -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; } diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SourceViewer.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SourceViewer.java index 78ed2c8a146..b37270ba014 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SourceViewer.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SourceViewer.java @@ -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) { diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/issues/Issue.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/issues/Issue.java index 997d7975e17..b5256a93499 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/issues/Issue.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/issues/Issue.java @@ -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(); diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/organization/MemberItem.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/organization/MemberItem.java index 8a4979aa4d3..466620af52b 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/organization/MemberItem.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/organization/MemberItem.java @@ -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; } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index 637a95e3fca..5280e553a75 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -22,14 +22,13 @@ import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import { SuggestionLink } from './SuggestionsProvider'; import { CurrentUser, isLoggedIn } from '../../types'; -import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/urls'; +import { DropdownOverlay } from '../../../components/controls/Dropdown'; interface Props { currentUser: CurrentUser; onClose: () => void; - popupPosition?: BubblePopupPosition; suggestions: Array; } @@ -46,7 +45,7 @@ export default class EmbedDocsPopup extends React.PureComponent { }; renderTitle(text: string) { - return
  • {text}
  • ; + return
  • {text}
  • ; } renderSuggestions() { @@ -147,10 +146,8 @@ export default class EmbedDocsPopup extends React.PureComponent { render() { return ( - -
      + +
        {this.renderSuggestions()}
      • @@ -165,7 +162,7 @@ export default class EmbedDocsPopup extends React.PureComponent { {this.context.onSonarCloud && this.renderSonarCloudLinks()} {!this.context.onSonarCloud && this.renderSonarQubeLinks()}
      - +
      ); } } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index 29b58e4e6e3..20ddca01c15 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -21,9 +21,9 @@ import * as React from 'react'; import EmbedDocsPopup from './EmbedDocsPopup'; import { SuggestionLink } from './SuggestionsProvider'; import { CurrentUser } from '../../types'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; -import HelpIcon from '../../../components/icons-components/HelpIcon'; +import Toggler from '../../../components/controls/Toggler'; import Tooltip from '../../../components/controls/Tooltip'; +import HelpIcon from '../../../components/icons-components/HelpIcon'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -63,6 +63,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent) => { event.preventDefault(); + event.currentTarget.blur(); this.toggleHelp(); }; @@ -78,26 +79,26 @@ export default class EmbedDocsPopupHelper extends React.PureComponent - } - position="bottomleft" - togglePopup={this.setHelpDisplay}> - - - - - - +
    • + + }> + + + + + + +
    • ); } } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap index b1af003796f..0984724bfcd 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap @@ -1,15 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display suggestion links 1`] = ` - +
      • embed_docs.suggestion
      • @@ -74,7 +72,7 @@ exports[`should display suggestion links 1`] = ` className="divider" />
      • embed_docs.get_support
      • @@ -114,7 +112,7 @@ exports[`should display suggestion links 1`] = ` className="divider" />
      • embed_docs.stay_connected
      • @@ -152,5 +150,5 @@ exports[`should display suggestion links 1`] = `
      -
      + `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 890e4d5f29e..a918d734e24 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; @@ -35,6 +34,7 @@ import { import { translate } from '../../../../helpers/l10n'; import PlusCircleIcon from '../../../../components/icons-components/PlusCircleIcon'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; +import Toggler from '../../../../components/controls/Toggler'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { @@ -91,19 +91,6 @@ export default class ComponentNavBranch extends React.PureComponent { - const { configuration } = this.props.component; - return this.state.dropdownOpen ? ( - - ) : null; - }; - renderMergeBranch = () => { const { currentBranchLike } = this.props; if (isShortLivingBranch(currentBranchLike)) { @@ -140,6 +127,7 @@ export default class ComponentNavBranch extends React.PureComponent - - - - {displayName} - - - - {this.renderDropdown()} +
      + {this.renderMergeBranch()}
      ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index 5772988c123..98f8de579cf 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -36,6 +36,7 @@ import { translate } from '../../../../helpers/l10n'; import { getBranchLikeUrl } from '../../../../helpers/urls'; import SearchBox from '../../../../components/controls/SearchBox'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; +import { DropdownOverlay } from '../../../../components/controls/Dropdown'; interface Props { branchLikes: BranchLike[]; @@ -51,7 +52,6 @@ interface State { } export default class ComponentNavBranchesMenu extends React.PureComponent { - private node?: HTMLElement | null; private listNode?: HTMLUListElement | null; private selectedBranchNode?: HTMLLIElement | null; @@ -59,13 +59,9 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { - 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) => { @@ -212,7 +198,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent {showDivider &&
    • } {showOrphanHeader && ( -
    • +
    • {translate('branches.orphan_branches')}
      @@ -223,12 +209,12 @@ export default class ComponentNavBranchesMenu extends React.PureComponent )} {showPullRequestHeader && ( -
    • +
    • {translate('branches.pull_requests')}
    • )} {showShortLivingBranchHeader && ( -
    • +
    • {translate('branches.short_lived_branches')}
    • )} @@ -261,7 +247,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent (this.node = node)}> + {this.renderSearch()} {this.renderBranchesList()} {showManageLink && ( @@ -273,7 +259,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent )} - + ); } } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 3a78c92dc46..86ea6e3c84a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -183,19 +183,21 @@ export default class ComponentNavMenu extends React.PureComponent { } return ( - + {adminLinks}
    } + tagName="li"> {({ onToggleClick, open }) => ( -
  • - - {translate('layout.settings')} - - -
      {adminLinks}
    -
  • + + {translate('layout.settings')} + + )} ); @@ -421,19 +423,21 @@ export default class ComponentNavMenu extends React.PureComponent { } return ( - + {extensions.map(e => this.renderExtension(e, false))}} + tagName="li"> {({ onToggleClick, open }) => ( -
  • - - {translate('more')} - - -
      {extensions.map(e => this.renderExtension(e, false))}
    -
  • + + {translate('more')} + + )}
    ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 8ea67da8b97..83a894a3b38 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -99,9 +99,9 @@ it('opens menu', () => { />, { context: { branchesEnabled: true } } ); - expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); + expect(wrapper.find('Toggler').prop('open')).toBe(false); click(wrapper.find('a')); - expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); + expect(wrapper.find('Toggler').prop('open')).toBe(true); }); it('renders single branch popup', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index 4ac93c4fb00..a7b7bb95953 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -41,8 +41,8 @@ it('should work with extensions', () => { const wrapper = shallow(, { 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(, { context: { branchesEnabled: true } }); - expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot(); - expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); }); it('should work for short-living branches', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index dc23c7b4368..3d9fee6a289 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -2,36 +2,71 @@ exports[`renders main branch 1`] = ` `; @@ -49,39 +84,80 @@ exports[`renders no branch support popup 1`] = ` exports[`renders pull request 1`] = ` @@ -105,44 +181,95 @@ exports[`renders pull request 1`] = ` exports[`renders short-living branch 1`] = ` diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index cac918b4985..3296a5ced18 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders list 1`] = ` -
  • branches.pull_requests
  • @@ -80,7 +80,7 @@ exports[`renders list 1`] = ` className="divider" />
  • -
    + `; exports[`searches 1`] = ` -
  • branches.short_lived_branches
  • @@ -303,5 +303,5 @@ exports[`searches 1`] = ` /> -
    + `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 249f20e49dc..3abe30c25f4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -90,7 +90,82 @@ exports[`should work for all qualifiers 1`] = ` +
  • + + project_settings.page + +
  • +
  • + + project_branches.page + +
  • +
  • + + webhooks.page + +
  • +
  • + + deletion.page + +
  • + + } + tagName="li" /> `; @@ -185,7 +260,31 @@ exports[`should work for all qualifiers 2`] = ` +
  • + + project_settings.page + +
  • + + } + tagName="li" /> `; @@ -280,7 +379,31 @@ exports[`should work for all qualifiers 3`] = ` +
  • + + deletion.page + +
  • + + } + tagName="li" /> `; @@ -467,7 +590,31 @@ exports[`should work for all qualifiers 5`] = ` +
  • + + deletion.page + +
  • + + } + tagName="li" /> `; @@ -710,355 +857,291 @@ exports[`should work for short-living branches 1`] = ` `; exports[`should work with extensions 1`] = ` -
  • - - layout.settings - - -
      -
    • - + - project_settings.page - -
    • -
    • + ComponentFoo + +
    • +
    + } + tagName="li" +/> +`; + +exports[`should work with extensions 2`] = ` + - + - project_branches.page - -
  • -
  • - + project_settings.page + +
  • +
  • + - webhooks.page - -
  • -
  • - + project_branches.page + +
  • +
  • + - Foo - -
  • -
  • - + webhooks.page + +
  • +
  • + - deletion.page - -
  • - - -`; - -exports[`should work with extensions 2`] = ` -
  • - - more - - -
      -
    • - + Foo + +
    • +
    • + - ComponentFoo - -
    • -
    -
  • + > + deletion.page + + + + } + tagName="li" +/> `; exports[`should work with multiple extensions 1`] = ` -
  • - - layout.settings - - -
      -
    • - + - project_settings.page - -
    • -
    • - + ComponentFoo + +
    • +
    • + - project_branches.page - -
    • -
    • + ComponentBar + +
    • +
    + } + tagName="li" +/> +`; + +exports[`should work with multiple extensions 2`] = ` + - + - webhooks.page - -
  • -
  • - + project_settings.page + +
  • +
  • + - Foo - -
  • -
  • - + project_branches.page + +
  • +
  • + - Bar - -
  • -
  • - + webhooks.page + +
  • +
  • + - deletion.page - -
  • - - -`; - -exports[`should work with multiple extensions 2`] = ` -
  • - - more - - -
      -
    • - + Foo + +
    • +
    • + - ComponentFoo - -
    • -
    • - + Bar + +
    • +
    • + - ComponentBar - -
    • -
    -
  • + > + deletion.page + + + + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 866edfae805..9e1eabf9be6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -99,14 +99,12 @@ class GlobalNav extends React.PureComponent {
      -
    • - -
    • + {isLoggedIn(this.props.currentUser) && this.props.onSonarCloud && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index 443f074225c..cab30c446ae 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -153,19 +153,20 @@ export default class GlobalNavMenu extends React.PureComponent { return null; } return ( - + {withoutPortfolios.map(this.renderGlobalPageLink)}
    } + tagName="li"> {({ onToggleClick, open }) => ( -
  • - - {translate('more')} - - -
      {withoutPortfolios.map(this.renderGlobalPageLink)}
    -
  • + + {translate('more')} + + )}
    ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 1ae29cfc8ab..d58fb2379ac 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm'; import PlusIcon from '../../../../components/icons-components/PlusIcon'; @@ -65,35 +64,41 @@ export default class GlobalNavPlus extends React.PureComponent { render() { return ( - + +
  • + + {translate('my_account.analyze_new_project')} + +
  • +
  • +
  • + + {translate('my_account.create_new_organization')} + +
  • + + } + tagName="li"> {({ onToggleClick, open }) => ( -
  • - + <> + - + {this.state.createOrganization && ( )} -
  • + )}
    ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index b568f713458..af85d305412 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { sortBy } from 'lodash'; import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; @@ -63,52 +62,52 @@ export default class GlobalNavUser extends React.PureComponent { const currentUser = this.props.currentUser as LoggedInUser; const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; return ( - - {({ onToggleClick, open }) => ( -
  • - - - -
      -
    • -
      - {currentUser.name} + +
    • +
      + {currentUser.name} +
      + {currentUser.email != null && ( +
      + {currentUser.email}
      - {currentUser.email != null && ( -
      - {currentUser.email} -
      - )} -
    • -
    • -
    • - {translate('my_account.page')} -
    • - {hasOrganizations &&
    • } - {hasOrganizations && ( -
    • - {translate('my_organizations')} -
    • )} - {hasOrganizations && - sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( - - ))} - {hasOrganizations &&
    • } +
    • +
    • +
    • + {translate('my_account.page')} +
    • + {hasOrganizations &&
    • } + {hasOrganizations && (
    • - - {translate('layout.logout')} - + {translate('my_organizations')}
    • -
    -
  • - )} + )} + {hasOrganizations && + sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + + ))} + {hasOrganizations &&
  • } +
  • + + {translate('layout.logout')} + +
  • + + } + tagName="li"> + + +
    ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 22fa861f34f..ba791499f59 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -32,7 +32,7 @@ it('should work with extensions', () => { const wrapper = shallow( ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should show administration menu if the user has the rights', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx index 1e02139f691..3e4110f5193 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx @@ -25,14 +25,16 @@ import { click } from '../../../../../helpers/testUtils'; it('render', () => { const wrapper = shallow(); 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() - .find('Dropdown') - .shallow(); + const wrapper = shallow( + shallow() + .find('Dropdown') + .prop('overlay') + ); click(wrapper.find('.js-new-project')); expect(openOnboardingTutorial).toBeCalled(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index ec18ecba3a9..152efb5d69c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => { ); 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', () => { ); wrapper.setState({ open: true }); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should not render user organizations when they are not activated', () => { @@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => { /> ); wrapper.setState({ open: true }); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap index 27714386573..6eb526f021f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap @@ -77,34 +77,22 @@ exports[`should show administration menu if the user has the rights 1`] = ` `; exports[`should work with extensions 1`] = ` -
  • - - more - - -
      -
    • - - Foo - -
    • -
    -
  • +
  • + + Foo + +
  • + + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap index 4400a02b250..1664c15d58c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap @@ -1,40 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`render 1`] = ` -
  • - - - - -
  • + +
  • + + my_account.analyze_new_project + +
  • +
  • +
  • + + my_account.create_new_organization + +
  • + + } + tagName="li" +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap index e83123dfbdc..316ff4a8e44 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap @@ -1,13 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should not render user organizations when they are not activated 1`] = ` -
  • +
  • +
    + + foo + +
    +
    + foo@bar.baz +
    +
  • +
  • +
  • + + my_account.page + +
  • +
  • + + layout.logout + +
  • + + } + tagName="li" > - - +
    `; 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`] = ` -
  • +
  • +
    + + foo + +
    +
    + foo@bar.baz +
    +
  • +
  • +
  • + + my_account.page + +
  • +
  • + + layout.logout + +
  • + + } + tagName="li" > - - +
    `; exports[`should render user organizations 1`] = ` -
  • +
  • +
    + + foo + +
    +
    + foo@bar.baz +
    +
  • +
  • +
  • + + my_account.page + +
  • +
  • +
  • + + my_organizations + +
  • + + + +
  • +
  • + + layout.logout + +
  • + + } + tagName="li" > -
      -
    • -
      - - foo - -
      -
      - foo@bar.baz -
      -
    • -
    • -
    • - - my_account.page - -
    • -
    • -
    • - - my_organizations - -
    • - - - -
    • -
    • - - layout.logout - -
    • -
    - +
    `; diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 8379d1822bb..ff656a311a5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -91,51 +91,54 @@ export default class SettingsNav extends React.PureComponent { extension => extension.key !== 'license/support' ); return ( - - {({ onToggleClick, open }) => ( -
  • - - {translate('sidebar.project_settings')} - - -
      -
    • - - {translate('settings.page')} - -
    • -
    • - - {translate('property.category.security.encryption')} - -
    • + +
    • + + {translate('settings.page')} + +
    • +
    • + + {translate('property.category.security.encryption')} + +
    • +
    • + + {translate('custom_metrics.page')} + +
    • + {!organizationsEnabled && (
    • - - {translate('custom_metrics.page')} + + {translate('webhooks.page')}
    • - {!organizationsEnabled && ( -
    • - - {translate('webhooks.page')} - -
    • - )} - {extensionsWithoutSupport.map(this.renderExtension)} -
    -
  • + )} + {extensionsWithoutSupport.map(this.renderExtension)} + + } + tagName="li"> + {({ onToggleClick, open }) => ( + + {translate('sidebar.project_settings')} + + )}
    ); @@ -144,30 +147,33 @@ export default class SettingsNav extends React.PureComponent { renderProjectsTab() { const { organizationsEnabled } = this.props; return ( - - {({ onToggleClick, open }) => ( -
  • - - {translate('sidebar.projects')} - -
      - {!organizationsEnabled && ( -
    • - - {translate('management')} - -
    • - )} + + {!organizationsEnabled && (
    • - - {translate('background_tasks.page')} + + {translate('management')}
    • -
    -
  • + )} +
  • + + {translate('background_tasks.page')} + +
  • + + } + tagName="li"> + {({ onToggleClick, open }) => ( + + {translate('sidebar.projects')} + )}
    ); @@ -176,44 +182,47 @@ export default class SettingsNav extends React.PureComponent { renderSecurityTab() { const { organizationsEnabled } = this.props; return ( - - {({ onToggleClick, open }) => ( -
  • - - {translate('sidebar.security')} - -
      + +
    • + + {translate('users.page')} + +
    • + {!organizationsEnabled && (
    • - - {translate('users.page')} + + {translate('user_groups.page')}
    • - {!organizationsEnabled && ( -
    • - - {translate('user_groups.page')} - -
    • - )} - {!organizationsEnabled && ( -
    • - - {translate('global_permissions.page')} - -
    • - )} - {!organizationsEnabled && ( -
    • - - {translate('permission_templates')} - -
    • - )} -
    -
  • + )} + {!organizationsEnabled && ( +
  • + + {translate('global_permissions.page')} + +
  • + )} + {!organizationsEnabled && ( +
  • + + {translate('permission_templates')} + +
  • + )} + + } + tagName="li"> + {({ onToggleClick, open }) => ( + + {translate('sidebar.security')} + )}
    ); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index ae3f5e900af..256662531cb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -33,5 +33,5 @@ it('should work with extensions', () => { /> ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('Dropdown').map(x => x.dive())).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index d7495bbbfc3..29a88938c4b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -14,9 +14,123 @@ exports[`should work with extensions 1`] = ` - - - + +
  • + + settings.page + +
  • +
  • + + property.category.security.encryption + +
  • +
  • + + custom_metrics.page + +
  • +
  • + + webhooks.page + +
  • +
  • + + Foo + +
  • + + } + tagName="li" + /> + +
  • + + users.page + +
  • +
  • + + user_groups.page + +
  • +
  • + + global_permissions.page + +
  • +
  • + + permission_templates + +
  • + + } + tagName="li" + /> + +
  • + + management + +
  • +
  • + + background_tasks.page + +
  • + + } + tagName="li" + />
  • - - sidebar.project_settings - - -
      -
    • - - settings.page - -
    • -
    • - - property.category.security.encryption - -
    • -
    • - - custom_metrics.page - -
    • -
    • - - webhooks.page - -
    • -
    • - - Foo - -
    • -
    -
  • , -
  • - - sidebar.security - - - -
      -
    • - - users.page - -
    • -
    • - - user_groups.page - -
    • -
    • - - global_permissions.page - -
    • -
    • - - permission_templates - -
    • -
    -
  • , -
  • - - sidebar.projects - - - -
      -
    • - - management - -
    • -
    • - - background_tasks.page - -
    • -
    -
  • , +
  • + + settings.page + +
  • +
  • + + property.category.security.encryption + +
  • +
  • + + custom_metrics.page + +
  • +
  • + + webhooks.page + +
  • +
  • + + Foo + +
  • + + } + tagName="li" + />, + +
  • + + users.page + +
  • +
  • + + user_groups.page + +
  • +
  • + + global_permissions.page + +
  • +
  • + + permission_templates + +
  • + + } + tagName="li" + />, + +
  • + + management + +
  • +
  • + + background_tasks.page + +
  • + + } + tagName="li" + />, ] `; diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js index 5912990d256..327844759f8 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.js +++ b/server/sonar-web/src/main/js/app/components/search/Search.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import key from 'keymaster'; import { debounce, keyBy, uniqBy } from 'lodash'; import { FormattedMessage } from 'react-intl'; @@ -31,12 +30,14 @@ import { sortQualifiers } from './utils'; import RecentHistory from '../../components/RecentHistory'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import ClockIcon from '../../../components/common/ClockIcon'; +import OutsideClickHandler from '../../../components/controls/OutsideClickHandler'; import SearchBox from '../../../components/controls/SearchBox'; import { getSuggestions } from '../../../api/components'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import { getProjectUrl } from '../../../helpers/urls'; import './Search.css'; +import { DropdownOverlay } from '../../../components/controls/Dropdown'; /*:: type Props = {| @@ -115,17 +116,22 @@ export default class Search extends React.PureComponent { componentWillUnmount() { this.mounted = false; key.unbind('s'); - window.removeEventListener('click', this.handleClickOutside); } - handleClickOutside = (event /*: { target: HTMLElement } */) => { - if (!this.node || !this.node.contains(event.target)) { - this.closeSearch(false); + handleClickOutside = () => { + this.closeSearch(false); + }; + + handleFocus = () => { + // simulate click to close any other dropdowns + const body = document.documentElement; + if (body) { + body.click(); } + this.openSearch(); }; openSearch = () => { - window.addEventListener('click', this.handleClickOutside); if (!this.state.open && !this.state.query) { this.search(''); } @@ -136,7 +142,6 @@ export default class Search extends React.PureComponent { if (this.input) { this.input.blur(); } - window.removeEventListener('click', this.handleClickOutside); this.setState( clear ? { @@ -304,10 +309,6 @@ export default class Search extends React.PureComponent { this.setState({ selected }); }; - handleClick = (event /*: Event */) => { - event.stopPropagation(); - }; - innerRef = (component /*: string */, node /*: HTMLElement */) => { this.nodes[component] = node; }; @@ -337,61 +338,65 @@ export default class Search extends React.PureComponent { ); render() { - const dropdownClassName = classNames('dropdown', 'navbar-search', { open: this.state.open }); - - return ( -
  • + const search = ( +
  • {this.state.shortQuery && ( - + {translateWithParameters('select2.tooShort', 2)} )} {this.state.open && Object.keys(this.state.results).length > 0 && ( -
    (this.node = node)}> - -
    -
    - - {translate('recently_browsed')} -
    - s - }} + +
    (this.node = node)}> + +
    +
    + + {translate('recently_browsed')} +
    + s + }} + /> +
    -
    + )}
  • ); + + return this.state.open ? ( + {search} + ) : ( + search + ); } } diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.js b/server/sonar-web/src/main/js/app/components/search/SearchResults.js index c22b619639d..2d8e3b7532f 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResults.js +++ b/server/sonar-web/src/main/js/app/components/search/SearchResults.js @@ -49,12 +49,12 @@ export default class SearchResults extends React.PureComponent { const components = this.props.results[qualifier]; if (components.length > 0 && renderedComponents.length > 0) { - renderedComponents.push(
  • ); + renderedComponents.push(
  • ); } if (components.length > 0) { renderedComponents.push( -
  • +
  • {translate('qualifiers', qualifier)}
  • ); diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js index 2e300a88cda..01168e7a49b 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js @@ -94,13 +94,3 @@ it('shows warning about short input', () => { form.setState({ query: 'foobar x' }); expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); }); - -it('closes on click outside', () => { - const form = mount( - - ); - form.instance().openSearch(); - expect(form.state().open).toBe(true); - clickOutside(); - expect(form.state().open).toBe(false); -}); diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap index 432b5562168..f13cf142896 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap @@ -5,7 +5,7 @@ exports[`renders "Show More" link 1`] = ` className="menu" >
  • qualifiers.TRK @@ -34,7 +34,7 @@ exports[`renders "Show More" link 1`] = ` key="divider-BRC" />
  • qualifiers.BRC @@ -57,7 +57,7 @@ exports[`renders different components and dividers between them 1`] = ` className="menu" >
  • qualifiers.TRK @@ -77,7 +77,7 @@ exports[`renders different components and dividers between them 1`] = ` key="divider-BRC" />
  • qualifiers.BRC @@ -97,7 +97,7 @@ exports[`renders different components and dividers between them 1`] = ` key="divider-FIL" />
  • qualifiers.FIL diff --git a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css b/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css deleted file mode 100644 index 031edf980a0..00000000000 --- a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -.bubble-popup { - position: absolute; - z-index: var(--bubblePopupZIndex); - margin-top: -16px; - margin-left: 8px; - padding: 10px; - border: 1px solid var(--barBorderColor); - border-radius: 3px; - box-sizing: border-box; - background-color: #ffffff; - box-shadow: var(--defaultShadow); - cursor: default; -} - -.bubble-popup-menu { - padding: 0; -} - -.bubble-popup-arrow, -.bubble-popup-arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border: 8px solid transparent; -} - -.bubble-popup-arrow { - top: 15px; - left: -8px; - border-left-width: 0; - border-right-color: var(--barBorderColor); -} - -.bubble-popup-arrow:after { - content: ' '; - left: 1px; - bottom: -8px; - border-left-width: 0; - border-right-color: #ffffff; -} - -.bubble-popup-bottom { - margin-top: 8px; - margin-left: -16px; -} - -.bubble-popup-bottom .bubble-popup-arrow { - top: -8px; - left: 15px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: var(--barBorderColor); -} - -.bubble-popup-bottom .bubble-popup-arrow:after { - left: -8px; - bottom: -9px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: #ffffff; -} - -.bubble-popup-bottom-right { - margin-top: 8px; - margin-left: -16px; - margin-left: 0; - margin-right: -16px; -} - -.bubble-popup-bottom-right .bubble-popup-arrow { - top: -8px; - left: 15px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: var(--barBorderColor); -} - -.bubble-popup-bottom-right .bubble-popup-arrow:after { - left: -8px; - bottom: -9px; - border-left-width: 8px; - border-top-width: 0; - border-right-color: transparent; - border-bottom-color: #ffffff; -} - -.bubble-popup-bottom-right .bubble-popup-arrow { - left: auto; - right: 15px; -} - -.bubble-popup-right { - margin-left: -8px; -} - -.bubble-popup-right .bubble-popup-arrow { - right: -8px; - left: auto; - border-right-width: 0; - border-left-width: 8px; - border-left-color: var(--barBorderColor); - border-right-color: transparent; -} - -.bubble-popup-right .bubble-popup-arrow:after { - left: auto; - right: 1px; - bottom: -8px; - border-right-width: 0; - border-left-width: 8px; - border-left-color: #ffffff; - border-right-color: transparent; -} - -.bubble-popup-container { - max-width: 560px; - max-height: 300px; - padding-right: 30px; - overflow: auto; -} - -.bubble-popup-helper { - position: relative; -} - -.bubble-popup-helper:focus { - outline: none; -} - -.bubble-popup-helper-inline { - display: inline-block; -} - -.bubble-popup-title { - margin-bottom: 5px; - font-weight: 600; -} - -.bubble-popup-section { - width: 450px; - padding-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.bubble-popup-section.fluid { - width: auto; - max-width: 450px; -} - -.bubble-popup-section + .bubble-popup-section, -.bubble-popup-section + .bubble-popup-title { - margin-top: 10px; -} - -.bubble-popup-list { - margin-top: 5px; -} - -.bubble-popup-list > li { - padding: 2px 0; -} diff --git a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css index d08b64ea06b..ef8e2bd3f10 100644 --- a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css +++ b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css @@ -19,156 +19,12 @@ */ .dropdown { position: relative; -} - -.dropdown-toggle:focus { - outline: 0; -} - -.dropdown-menu { - min-width: 160px; - padding: 5px 0; - list-style: none; - font-size: var(--smallFontSize); - text-align: left; - background-color: #fff; - position: absolute; - top: 100%; - left: 0; - z-index: var(--dropdownMenuZIndex); - display: none; - float: left; - border: 1px solid var(--barBorderColor); - background-clip: padding-box; - box-shadow: var(--defaultShadow); -} - -.dropdown-menu:focus { - outline: none; -} - -.dropdown-menu > li > a, -.dropdown-menu > li > span { - display: block; - padding: 4px 16px; - line-height: 16px; - clear: both; - font-weight: normal; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dropdown-menu > li > a { - color: var(--baseFontColor); - border-bottom: none; - transition: none; -} - -.dropdown-menu > li > a.text-danger, -.dropdown-menu > li > a.text-danger:hover { - color: var(--red) !important; -} - -.dropdown-menu .divider { - height: 1px; - margin: 6px 0; - overflow: hidden; - background-color: var(--barBorderColor); -} - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - text-decoration: none; - color: var(--baseFontColor); - background-color: var(--barBackgroundColor); -} - -.dropdown-menu > .active > a, -.dropdown-menu > li > .active, -.dropdown-menu > .active > a:hover, -.dropdown-menu > li > .active:hover, -.dropdown-menu > .active > a:focus, -.dropdown-menu > li > .active:focus { - color: var(--baseFontColor); - text-decoration: none; - outline: 0; - background-color: var(--barBackgroundColor); -} - -.dropdown-menu .menu-vertically-limited { - max-height: 300px; - overflow-y: auto; -} - -.dropdown-menu .menu-footer > a > span { - border-bottom: 1px solid var(--gray80); - color: var(--secondFontColor); -} - -.dropdown-menu .menu-footer-note { - opacity: 0; - transition: opacity 0.3s ease; -} - -.dropdown-menu .menu-footer.active .menu-footer-note { - opacity: 1; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.open > .dropdown-menu { - display: block; -} - -.open > a { - outline: 0; -} - -.dropdown-menu-right { - left: auto; - right: 0; -} - -.dropdown-menu-left { - left: 0; - right: auto; -} - -.dropdown-header { - display: block; - padding: 3px 8px 5px; - font-size: var(--smallFontSize); - color: var(--secondFontColor); - white-space: nowrap; -} - -.dropdown-item { - padding: 5px 16px; -} - -.dropdown-menu .small-divider { - height: 1px; - margin: 4px 20px; - overflow: hidden; - background-color: var(--barBackgroundColor); -} - -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: 990; + display: inline-block; + vertical-align: middle; } .dropdown-bottom-hint { line-height: 16px; - margin-top: 5px; margin-bottom: -5px; padding: 5px 10px; border-top: 1px solid var(--barBorderColor); @@ -176,9 +32,3 @@ color: var(--secondFontColor); font-size: 11px; } - -.dropdown-item-flex { - display: flex !important; - justify-content: space-between; - align-items: center; -} diff --git a/server/sonar-web/src/main/js/app/styles/components/issues.css b/server/sonar-web/src/main/js/app/styles/components/issues.css index 3ff1e991902..21eb1a3066a 100644 --- a/server/sonar-web/src/main/js/app/styles/components/issues.css +++ b/server/sonar-web/src/main/js/app/styles/components/issues.css @@ -168,8 +168,7 @@ } .issue-changelog { - min-width: 450px; - max-width: 540px; + width: 450px; max-height: 320px; overflow: auto; white-space: normal; @@ -345,14 +344,9 @@ input.issue-action-options-search { .issue-comment-bubble-popup { width: 440px; - margin-left: -220px; font-size: var(--smallFontSize); } -.issue-comment-bubble-popup .bubble-popup-arrow { - left: 220px; -} - .issue-edit-comment-bubble-popup { width: 440px; font-size: var(--smallFontSize); @@ -481,7 +475,7 @@ input.issue-action-options-search { background-color: #ccc; } -.issue .menu:not(.issues-similar-issues-menu) { +.issue .menu:not(.issues-similar-issues-menu):not(.issue-changelog) { max-height: 120px; overflow: auto; } diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css index ac3fd543e5f..e5bf1c05eda 100644 --- a/server/sonar-web/src/main/js/app/styles/components/menu.css +++ b/server/sonar-web/src/main/js/app/styles/components/menu.css @@ -31,6 +31,11 @@ outline: none; } +.menu.is-container { + padding: 5px; +} + +.menu-item, .menu > li > a, .menu > li > span { display: block; @@ -85,6 +90,11 @@ background-color: var(--barBackgroundColor); } +.menu > li > a.text-danger, +.menu > li > a.text-danger:hover { + color: var(--red) !important; +} + .menu .menu-vertically-limited, .menu.menu-vertically-limited { max-height: 300px; @@ -115,7 +125,7 @@ .menu-search { position: relative; - padding: 4px 16px 0; + padding: var(--gridSize) calc(2 * var(--gridSize)) 0; } .menu-search .search-box, @@ -143,3 +153,15 @@ padding: 4px 16px; line-height: 16px; } + +.menu-header { + padding: var(--gridSize); + font-size: 12px; + color: #777; + white-space: nowrap; +} + +.menu-header:first-child, +.divider + .menu-header { + padding-top: calc(var(--gridSize) - 5px); +} diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css index 9e5a247cdd1..eaef6aa0909 100644 --- a/server/sonar-web/src/main/js/app/styles/sonar.css +++ b/server/sonar-web/src/main/js/app/styles/sonar.css @@ -29,7 +29,6 @@ @import './components/ui.css'; @import './components/spinner.css'; @import './components/global-loading.css'; -@import './components/bubble-popup.css'; @import './components/modals.css'; @import './components/alerts.css'; @import './components/issues.css'; diff --git a/server/sonar-web/src/main/js/app/styles/style.css b/server/sonar-web/src/main/js/app/styles/style.css index 4fc6c561948..61be8927c26 100644 --- a/server/sonar-web/src/main/js/app/styles/style.css +++ b/server/sonar-web/src/main/js/app/styles/style.css @@ -200,7 +200,6 @@ } .property pre, -.bubble-popup pre, .coding-rules-detail-parameter pre { display: inline-block; min-width: 100%; @@ -213,7 +212,6 @@ } .property blockquote, -.bubble-popup blockquote, .coding-rules-detail-parameter blockquote { margin-top: 10px; padding: 10px; diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 59808a135ba..e57fc075f6c 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -56,6 +56,7 @@ module.exports = { snippetFontColor: '#f0f0f0', // sizes + grid, gridSize: `${grid}px`, baseFontSize: '13px', @@ -105,5 +106,5 @@ module.exports = { modalZIndex: '6001', modalOverlayZIndex: '6000', - bubblePopupZIndex: '5000' + popupZIndex: '5000' }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx index 63d039166cc..6b47b2d704d 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import BulkChangeModal from './BulkChangeModal'; import { Query } from '../query'; import { Profile } from '../../../api/quality-profiles'; @@ -40,7 +39,6 @@ interface State { } export default class BulkChange extends React.PureComponent { - closeDropdown?: () => void; state: State = { modal: false }; getSelectedProfile = () => { @@ -53,36 +51,24 @@ export default class BulkChange extends React.PureComponent { handleActivateClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'activate', modal: true, profile: undefined }); }; handleActivateInProfileClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'activate', modal: true, profile: this.getSelectedProfile() }); }; handleDeactivateClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.setState({ action: 'deactivate', modal: true, profile: undefined }); }; handleDeactivateInProfileClick = (event: React.SyntheticEvent) => { 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 { return ( <> - - {({ closeDropdown, onToggleClick, open }) => { - this.closeDropdown = closeDropdown; - return ( - - ); - }} + )} + + }> + {this.state.modal && this.state.action && ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index 16e9f0d6f65..60d51aae6bd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -31,10 +31,11 @@ import DocTooltip from '../../../components/docs/DocTooltip'; import { translate } from '../../../helpers/l10n'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SeverityHelper from '../../../components/shared/SeverityHelper'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Dropdown from '../../../components/controls/Dropdown'; import TagsList from '../../../components/tags/TagsList'; import DateFormatter from '../../../components/intl/DateFormatter'; import { Button } from '../../../components/ui/buttons'; +import { PopupPlacement } from '../../../components/ui/popups'; interface Props { canWrite: boolean | undefined; @@ -46,19 +47,7 @@ interface Props { ruleDetails: RuleDetails; } -interface State { - tagsPopup: boolean; -} - -export default class RuleDetailsMeta extends React.PureComponent { - 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 { renderType = () => { const { ruleDetails } = this.props; return ( @@ -106,9 +95,10 @@ export default class RuleDetailsMeta extends React.PureComponent { return (
  • {this.props.canWrite ? ( - { tags={allTags} /> } - position="bottomleft" - togglePopup={this.handleTagsPopupToggle}> - - + ) : ( void; sysTags: string[]; tags: string[]; @@ -81,7 +79,6 @@ export default class RuleDetailsTagsPopup extends React.PureComponent diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx index 8340279f3ad..f7106afcc46 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { Query } from '../query'; import { Rule } from '../../../app/types'; import Dropdown from '../../../components/controls/Dropdown'; @@ -31,32 +30,21 @@ interface Props { } export default class SimilarRulesFilter extends React.PureComponent { - closeDropdown?: () => void; - handleLanguageClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.props.onFilterChange({ languages: [this.props.rule.lang] }); }; handleTypeClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); - if (this.closeDropdown) { - this.closeDropdown(); - } this.props.onFilterChange({ types: [this.props.rule.type] }); }; handleSeverityClick = (event: React.SyntheticEvent) => { 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 { handleTagClick = (event: React.SyntheticEvent) => { 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 { const allTags = [...tags, ...sysTags]; return ( - - {({ closeDropdown, onToggleClick, open }) => { - this.closeDropdown = closeDropdown; - return ( -
    - - - - -
    -
    - {translate('coding_rules.filter_similar_rules')} -
    -
    -
    - ); - }} + ))} + + )} + + + }> + + + +
    ); } diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx deleted file mode 100644 index f4f2ff15d24..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { CustomMeasure } from '../../../app/types'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -interface Props { - measure: CustomMeasure; - onDelete: (measureId: string) => Promise; -} - -export default function DeleteButton({ measure, onDelete }: Props) { - return ( - - {({ onClick }) => ( - - {translate('delete')} - - )} - - ); -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx new file mode 100644 index 00000000000..f8da5faad95 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { CustomMeasure } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + measure: CustomMeasure; + onClose: () => void; + onSubmit: () => Promise; +} + +export default function DeleteForm({ measure, onClose, onSubmit }: Props) { + const header = translate('custom_measures.delete_custom_measure'); + + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( +
    +
    +

    {header}

    +
    + +
    + {translateWithParameters( + 'custom_measures.delete_custom_measure.confirmation', + measure.metric.name + )} +
    + +
    + + + {translate('delete')} + + + {translate('cancel')} + +
    +
    + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx deleted file mode 100644 index 655b02b6eaf..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form from './Form'; -import { CustomMeasure } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; - -interface Props { - measure: CustomMeasure; - onEdit: (data: { description: string; id: string; value: string }) => Promise; -} - -interface State { - modal: boolean; -} - -export default class EditButton extends React.PureComponent { - 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 ( - <> - - {translate('update_verb')} - - {this.state.modal && ( -
    - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx new file mode 100644 index 00000000000..335d89faacd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx @@ -0,0 +1,158 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeleteForm from './DeleteForm'; +import Form from './Form'; +import MeasureDate from './MeasureDate'; +import { CustomMeasure } from '../../../app/types'; +import ActionsDropdown, { + ActionsDropdownDivider, + ActionsDropdownItem +} from '../../../components/controls/ActionsDropdown'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; + +interface Props { + measure: CustomMeasure; + onDelete: (measureId: string) => Promise; + onEdit: (data: { description: string; id: string; value: string }) => Promise; +} + +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class Item extends React.PureComponent { + 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 ( + + +
    + {measure.metric.name} + {measure.pending && ( + + + {translate('custom_measures.pending')} + + + )} +
    + {measure.metric.domain} + + + + + {formatMeasure(measure.value, measure.metric.type)} + + + + + {measure.description} + + + + {translate('by_')}{' '} + {measure.user.name} + + + + + + {translate('update_verb')} + + + + {translate('delete')} + + + + + {this.state.editForm && ( + + )} + + {this.state.deleteForm && ( + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx index 239b4eeec6c..f26474192d1 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx @@ -19,16 +19,9 @@ */ import * as React from 'react'; import { sortBy } from 'lodash'; -import DeleteButton from './DeleteButton'; -import EditButton from './EditButton'; +import Item from './Item'; import { CustomMeasure } from '../../../app/types'; -import ActionsDropdown, { - ActionsDropdownDivider -} from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; -import Tooltip from '../../../components/controls/Tooltip'; -import { formatMeasure } from '../../../helpers/measures'; -import DateFormatter from '../../../components/intl/DateFormatter'; interface Props { measures: CustomMeasure[]; @@ -52,44 +45,7 @@ export default function List({ measures, onDelete, onEdit }: Props) { {sortBy(measures, measure => measure.metric.name.toLowerCase()).map(measure => ( - - -
    - {measure.metric.name} - {measure.pending && ( - - - {translate('custom_measures.pending')} - - - )} -
    - {measure.metric.domain} - - - - - {formatMeasure(measure.value, measure.metric.type)} - - - - - {measure.description} - - - - {translate('by_')}{' '} - {measure.user.name} - - - - - - - - - - + ))} @@ -99,27 +55,3 @@ export default function List({ measures, onDelete, onEdit }: Props) {
  • ); } - -function MeasureDate({ measure }: { measure: CustomMeasure }) { - if (measure.updatedAt) { - return ( - <> - {translate('updated_on')}{' '} - - - - - ); - } else if (measure.createdAt) { - return ( - <> - {translate('created_on')}{' '} - - - - - ); - } else { - return <>{translate('created')}; - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx new file mode 100644 index 00000000000..cde3f66e28d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { CustomMeasure } from '../../../app/types'; +import DateFormatter from '../../../components/intl/DateFormatter'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + measure: CustomMeasure; +} + +export default function MeasureDate({ measure }: Props) { + if (measure.updatedAt) { + return ( + <> + {translate('updated_on')}{' '} + + + + + ); + } else if (measure.createdAt) { + return ( + <> + {translate('created_on')}{' '} + + + + + ); + } else { + return <>{translate('created')}; + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx similarity index 77% rename from server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx rename to server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx index 3c032dafc62..411e81c2d7e 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx @@ -19,9 +19,9 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import DeleteButton from '../DeleteButton'; +import DeleteForm from '../DeleteForm'; -it('should delete custom measure', () => { +it('should render', () => { const measure = { createdAt: '2017-01-01', description: 'my custom measure', @@ -31,10 +31,7 @@ it('should delete custom measure', () => { user: { active: true, login: 'user', name: 'user' }, value: 'custom-value' }; - const onDelete = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('ConfirmButton').prop('onConfirm')('1'); - expect(onDelete).toBeCalledWith('1'); + expect( + shallow().dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx similarity index 60% rename from server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx rename to server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx index f1efbc3a527..16942dfd967 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx @@ -19,27 +19,31 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import EditButton from '../EditButton'; +import Item from '../Item'; import { click } from '../../../../helpers/testUtils'; +const measure = { + createdAt: '2017-01-01', + description: 'my custom measure', + id: '1', + metric: { key: 'custom', name: 'custom-metric', type: 'STRING' }, + projectKey: 'foo', + user: { active: true, login: 'user', name: 'user' }, + value: 'custom-value' +}; + +it('should render', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + it('should edit metric', () => { - const measure = { - createdAt: '2017-01-01', - description: 'my custom measure', - id: '1', - metric: { key: 'custom', name: 'custom-metric', type: 'STRING' }, - projectKey: 'foo', - user: { active: true, login: 'user', name: 'user' }, - value: 'custom-value' - }; const onEdit = jest.fn(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow(); click(wrapper.find('.js-custom-measure-update')); wrapper.update(); - expect(wrapper).toMatchSnapshot(); wrapper.find('Form').prop('onSubmit')({ ...measure, @@ -48,3 +52,14 @@ it('should edit metric', () => { }); expect(onEdit).toBeCalledWith({ ...measure, description: 'new-description', value: 'new-value' }); }); + +it('should delete custom measure', () => { + const onDelete = jest.fn(); + const wrapper = shallow(); + + click(wrapper.find('.js-custom-measure-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop('onSubmit')(); + expect(onDelete).toBeCalledWith('1'); +}); diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap deleted file mode 100644 index c9fc5439615..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should delete custom measure 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..77d8286a180 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
    +

    + custom_measures.delete_custom_measure +

    +
    +
    + custom_measures.delete_custom_measure.confirmation.custom-metric +
    +
    + + + delete + + + cancel + +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap deleted file mode 100644 index bd0a8035083..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit metric 1`] = ` - - - update_verb - - -`; - -exports[`should edit metric 2`] = ` - - - update_verb - -
    - -`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap new file mode 100644 index 00000000000..225ca0c587c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
    + + custom-metric + +
    + + + + + custom-value + + + + + my custom measure + + + + + + by_ + + + user + + + + + + update_verb + + + + delete + + + + +`; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap index 005bedff1e0..14d2a0c362d 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap @@ -26,235 +26,53 @@ exports[`should render 1`] = ` - - -
    - - another-metric - -
    - - - - - another-value - - - - - - - - - by_ - - - user - - - - - - - - - - - + - -
    - - custom-metric - -
    - - - - - custom-value - - - - - my custom measure - - - - - - by_ - - - user - - - - - - - - - - + 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]} + /> diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx deleted file mode 100644 index d825823c7f3..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { Metric } from '../../../app/types'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -interface Props { - metric: Metric; - onDelete: (metricKey: string) => Promise; -} - -export default function DeleteButton({ metric, onDelete }: Props) { - return ( - - {({ onClick }) => ( - - {translate('delete')} - - )} - - ); -} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx new file mode 100644 index 00000000000..c799bcb1a56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Metric } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + metric: Metric; + onClose: () => void; + onSubmit: () => Promise; +} + +export default function DeleteForm({ metric, onClose, onSubmit }: Props) { + const header = translate('custom_metrics.delete_metric'); + + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( + +
    +

    {header}

    +
    + +
    + {translateWithParameters('custom_metrics.delete_metric.confirmation', metric.name)} +
    + +
    + + + {translate('delete')} + + + {translate('cancel')} + +
    + + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx deleted file mode 100644 index d0870fa8c06..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form, { MetricProps } from './Form'; -import { Metric } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; - -interface Props { - domains: string[]; - metric: Metric; - onEdit: (data: { id: string } & MetricProps) => Promise; - types: string[]; -} - -interface State { - modal: boolean; -} - -export default class EditButton extends React.PureComponent { - 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 ( - <> - - {translate('update_details')} - - {this.state.modal && ( -
    - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx new file mode 100644 index 00000000000..7f31c648bf3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx @@ -0,0 +1,150 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeleteForm from './DeleteForm'; +import Form, { MetricProps } from './Form'; +import { Metric } from '../../../app/types'; +import ActionsDropdown, { + ActionsDropdownDivider, + ActionsDropdownItem +} from '../../../components/controls/ActionsDropdown'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + domains?: string[]; + metric: Metric; + onDelete: (metricKey: string) => Promise; + onEdit: (data: { id: string } & MetricProps) => Promise; + types?: string[]; +} + +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class Item extends React.PureComponent { + 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 ( + + +
    + {metric.name} + {metric.key} +
    + + + + {metric.domain} + + + + {translate('metric.type', metric.type)} + + + + {metric.description} + + + + + {domains && + types && ( + + {translate('update_details')} + + )} + + + {translate('delete')} + + + + + {this.state.editForm && + domains && + types && ( + + )} + + {this.state.deleteForm && ( + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx index a5449bf68b6..ddbf383ec2d 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx @@ -19,13 +19,9 @@ */ import * as React from 'react'; import { sortBy } from 'lodash'; -import DeleteButton from './DeleteButton'; -import EditButton from './EditButton'; import { MetricProps } from './Form'; +import Item from './Item'; import { Metric } from '../../../app/types'; -import ActionsDropdown, { - ActionsDropdownDivider -} from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -43,42 +39,14 @@ export default function List({ domains, metrics, onDelete, onEdit, types }: Prop {sortBy(metrics, metric => metric.name.toLowerCase()).map(metric => ( - - - - - - - - - - - + ))}
    -
    - {metric.name} - {metric.key} -
    -
    - {metric.domain} - - {translate('metric.type', metric.type)} - - {metric.description} - - - {domains && - types && ( - - )} - - - -
    diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx similarity index 74% rename from server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx rename to server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx index a35e39a610e..bbfe2d0f2b1 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx @@ -19,14 +19,11 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import DeleteButton from '../DeleteButton'; +import DeleteForm from '../DeleteForm'; -it('should delete metric', () => { +it('should render', () => { const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; - const onDelete = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('ConfirmButton').prop('onConfirm')('foo'); - expect(onDelete).toBeCalledWith('foo'); + expect( + shallow().dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx similarity index 70% rename from server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx rename to server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx index 5fce34493a4..dc731037d09 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx @@ -19,26 +19,32 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import EditButton from '../EditButton'; +import Item from '../Item'; import { click } from '../../../../helpers/testUtils'; +const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; + +it('should render', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + it('should edit metric', () => { - const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' }; const onEdit = jest.fn(); const wrapper = shallow( - ); - expect(wrapper).toMatchSnapshot(); click(wrapper.find('.js-metric-update')); wrapper.update(); - expect(wrapper).toMatchSnapshot(); wrapper.find('Form').prop('onSubmit')({ ...metric, @@ -47,3 +53,14 @@ it('should edit metric', () => { }); expect(onEdit).toBeCalledWith({ ...metric, description: 'bla bla', domain: 'Coverage' }); }); + +it('should delete metric', () => { + const onDelete = jest.fn(); + const wrapper = shallow(); + + click(wrapper.find('.js-metric-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop('onSubmit')(); + expect(onDelete).toBeCalledWith('foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap deleted file mode 100644 index bd6e9cd420b..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should delete metric 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..cd0903c2c8a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
    +

    + custom_metrics.delete_metric +

    +
    +
    + custom_metrics.delete_metric.confirmation.Foo +
    +
    + + + delete + + + cancel + +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap deleted file mode 100644 index 66d078530d4..00000000000 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit metric 1`] = ` - - - update_details - - -`; - -exports[`should edit metric 2`] = ` - - - update_details - -
    - -`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap new file mode 100644 index 00000000000..dbef9673e8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
    + + Foo + + + foo + +
    + + + + + + + metric.type.INT + + + + + + + + + + delete + + + + +`; diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap index 1fb174588c7..31507c7f042 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap @@ -9,133 +9,33 @@ exports[`should render 1`] = ` className="data zebra zebra-hover" > - - -
    - - Bar - - - bar - -
    - - - - Coverage - - - - - metric.type.INT - - - - - - - - - - - - - + - -
    - - Foo - - - foo - -
    - - - - - - - metric.type.INT - - - - - - - - - - - - + metric={ + Object { + "id": "3", + "key": "foo", + "name": "Foo", + "type": "INT", + } + } + onDelete={[MockFunction]} + onEdit={[MockFunction]} + /> diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx new file mode 100644 index 00000000000..a1a3a495677 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Group } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + group: Group; + onClose: () => void; + onSubmit: () => Promise; +} + +export default function DeleteForm({ group, onClose, onSubmit }: Props) { + const header = translate('groups.delete_group'); + + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( + +
    +

    {header}

    +
    + +
    + {translateWithParameters('groups.delete_group.confirmation', group.name)} +
    + +
    + + + {translate('delete')} + + + {translate('cancel')} + +
    + + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx deleted file mode 100644 index 2de9ded54a1..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Form from './Form'; -import { Group } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; -import { omitNil } from '../../../helpers/request'; - -interface Props { - children: (props: { onClick: () => void }) => React.ReactNode; - group: Group; - onEdit: (data: { description?: string; id: number; name?: string }) => Promise; -} - -interface State { - modal: boolean; -} - -export default class EditGroup extends React.PureComponent { - 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 && ( -
    - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx index 651066f64b0..a1f2f8ba611 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx @@ -18,15 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import EditGroup from './EditGroup'; +import DeleteForm from './DeleteForm'; import EditMembers from './EditMembers'; +import Form from './Form'; import { Group } from '../../../app/types'; import ActionsDropdown, { ActionsDropdownItem, ActionsDropdownDivider } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; +import { omitNil } from '../../../helpers/request'; interface Props { group: Group; @@ -36,11 +37,57 @@ interface Props { organization: string | undefined; } -export default class ListItem extends React.PureComponent { - handleDelete = () => { +interface State { + deleteForm: boolean; + editForm: boolean; +} + +export default class ListItem extends React.PureComponent { + 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 { {!group.default && ( - - {({ onClick }) => ( - - {translate('update_details')} - - )} - + + {translate('update_details')} + - - {({ onClick }) => ( - - {translate('delete')} - - )} - + + {translate('delete')} + )} + + {this.state.deleteForm && ( + + )} + + {this.state.editForm && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx similarity index 73% rename from server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js rename to server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx index 750dd8a89a3..9aa65bc38a9 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx @@ -17,20 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; -import BubblePopup from '../BubblePopup'; +import DeleteForm from '../DeleteForm'; -const props = { - position: { top: 0, right: 0 }, - customClass: 'custom' -}; - -it('should render popup', () => { - const popup = shallow( - - test - - ); - expect(popup).toMatchSnapshot(); +it('should render', () => { + const group = { id: 3, name: 'Foo', membersCount: 5 }; + expect( + shallow().dive() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx deleted file mode 100644 index d650c321233..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { shallow } from 'enzyme'; -import EditGroup from '../EditGroup'; - -it('should edit group', () => { - const group = { id: 3, name: 'Foo', membersCount: 5 }; - const onEdit = jest.fn(); - const newDescription = 'bla bla'; - let onClick: any; - - const wrapper = shallow( - - {props => { - ({ onClick } = props); - return
    ; - }} - - ); - expect(wrapper).toMatchSnapshot(); - - onClick(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); - - // change name - wrapper.find('Form').prop('onSubmit')({ name: 'Bar', description: newDescription }); - expect(onEdit).lastCalledWith({ description: newDescription, id: 3, name: 'Bar' }); - - // change description - wrapper.find('Form').prop('onSubmit')({ - name: group.name, - description: newDescription - }); - expect(onEdit).lastCalledWith({ description: newDescription, id: group.id }); - - wrapper.find('Form').prop('onClose')(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx index 28c9bb1617a..805c151b5cb 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx @@ -20,6 +20,27 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ListItem from '../ListItem'; +import { click } from '../../../../helpers/testUtils'; + +it('should edit group', () => { + const group = { id: 3, name: 'Foo', membersCount: 5 }; + const onEdit = jest.fn(); + const wrapper = shallow( + + ); + + click(wrapper.find('.js-group-update')); + wrapper.update(); + + wrapper.find('Form').prop('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('onConfirm')(); + click(wrapper.find('.js-group-delete')); + wrapper.update(); + + wrapper.find('DeleteForm').prop('onSubmit')(); expect(onDelete).toBeCalledWith('Foo'); }); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap new file mode 100644 index 00000000000..4d93bc7cf40 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
    +

    + groups.delete_group +

    +
    +
    + groups.delete_group.confirmation.Foo +
    +
    + + + delete + + + cancel + +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap deleted file mode 100644 index 43bc8b832cb..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit group 1`] = ` - -
    - -`; - -exports[`should edit group 2`] = ` - -
    -
    - -`; - -exports[`should edit group 3`] = ` - -
    - -`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap index 2146de899da..59d94b8e22b 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap @@ -48,24 +48,20 @@ exports[`should delete group 1`] = ` className="thin nowrap text-right" > - + + update_details + - + + delete + diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index df3a3a1374c..95d0e0dacea 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import Helmet from 'react-helmet'; -import * as classNames from 'classnames'; import * as key from 'keymaster'; import { keyBy, union, without } from 'lodash'; import * as PropTypes from 'prop-types'; @@ -804,27 +803,26 @@ export default class App extends React.PureComponent { thirdState={thirdState} /> {checked.length > 0 ? ( - - {({ onToggleClick, open }) => ( - - )} + +
  • + + {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} + +
  • +
  • + + {translateWithParameters('issues.bulk_change_selected', checked.length)} + +
  • + + }> +
    ) : (
    - ); - } +export default function PluginChangeLogButton({ release, update }: Props) { + return ( + }> + -
    @@ -163,16 +151,4 @@ export default class ManageMemberGroupsForm extends React.PureComponent { ); } - - render() { - const buttonComponent = ( - - {translate('organization.members.manage_groups')} - - ); - if (this.state.open) { - return [buttonComponent, this.renderModal()]; - } - return buttonComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js index d4dae2fa781..343054e6be3 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js @@ -20,69 +20,48 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; /*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */ /*:: import type { Organization } from '../../../../store/organizations/duck'; */ /*:: type Props = { + onClose: () => void; member: Member, organization: Organization, removeMember: (member: Member) => void }; */ -/*:: -type State = { - open: boolean -}; -*/ - export default class RemoveMemberForm extends React.PureComponent { /*:: props: Props; */ - - state /*: State */ = { - open: false - }; - - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - this.setState({ open: false }); - }; - handleSubmit = (e /*: Object */) => { e.preventDefault(); this.props.removeMember(this.props.member); - this.closeForm(); + this.props.onClose(); }; - renderModal() { + render() { const header = translate('users.remove'); return ( - +

    {header}

    -
    -

    - {translateWithParameters( - 'organization.members.remove_x', - this.props.member.name, - this.props.organization.name - )} -

    +
    + {translateWithParameters( + 'organization.members.remove_x', + this.props.member.name, + this.props.organization.name + )}
    - -
    @@ -91,16 +70,4 @@ export default class RemoveMemberForm extends React.PureComponent { ); } - - render() { - const buttonComponent = ( - - {translate('organization.members.remove')} - - ); - if (this.state.open) { - return [buttonComponent, this.renderModal()]; - } - return buttonComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js index 2bd5905184b..c2d30153c74 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js @@ -54,19 +54,19 @@ function getMountedForm(updateFunc = jest.fn()) { const wrapper = shallow( + />, + { 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( { /> ); expect(wrapper).toMatchSnapshot(); - wrapper.setState({ open: true }); - expect(wrapper.first().getElements()).toMatchSnapshot(); -}); - -it('should correctly handle user interactions', () => { - const form = getMountedForm(); - form.wrapper.find('ActionsDropdownItem').prop('onClick')(); - expect(form.wrapper.state('open')).toBeTruthy(); - expect(form.instance.loadUserGroups).toBeCalled(); - expect(form.wrapper.state()).toMatchSnapshot(); }); it('should correctly select the groups', () => { const form = getMountedForm(); - form.instance.openForm(mockEvent); expect(form.instance.isGroupSelected(11)).toBeTruthy(); expect(form.instance.isGroupSelected(7)).toBeFalsy(); form.instance.onCheck(11, false); @@ -103,21 +92,9 @@ it('should correctly select the groups', () => { it('should correctly handle the submit event and close the modal', () => { const updateMemberGroups = jest.fn(); const form = getMountedForm(updateMemberGroups); - form.instance.openForm(mockEvent); form.instance.onCheck(11, false); form.instance.onCheck(7, true); form.instance.handleSubmit(mockEvent); expect(updateMemberGroups.mock.calls).toMatchSnapshot(); expect(form.wrapper.state()).toMatchSnapshot(); }); - -it('should reset the selected groups when the modal is opened', () => { - const form = getMountedForm(); - form.instance.openForm(mockEvent); - form.instance.onCheck(11, false); - form.instance.onCheck(7, true); - expect(form.wrapper.state()).toMatchSnapshot(); - form.instance.closeForm(); - form.instance.openForm(mockEvent); - expect(form.wrapper.state()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js index e02af92237a..fca12f0187b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js @@ -22,30 +22,31 @@ import { shallow } from 'enzyme'; import { click, mockEvent } from '../../../../../helpers/testUtils'; import RemoveMemberForm from '../RemoveMemberForm'; -jest.mock('react-dom'); - const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }; const organization = { name: 'MyOrg' }; -it('should render and open the modal', () => { +it('should render ', () => { const wrapper = shallow( - + ); 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( - + ); - const instance = wrapper.instance(); - wrapper.find('ActionsDropdownItem').prop('onClick')(); - expect(wrapper.state('open')).toBeTruthy(); - instance.handleSubmit(mockEvent); - expect(removeMember.mock.calls).toMatchSnapshot(); - instance.closeForm(); - expect(wrapper.state('open')).toBeFalsy(); + wrapper.instance().handleSubmit(mockEvent); + expect(removeMember).toBeCalledWith({ + avatar: '', + groupCount: 3, + login: 'admin', + name: 'Admin Istrator' + }); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap index 94ff9bb6705..2eea5b66085 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap @@ -22,7 +22,6 @@ Array [ exports[`should correctly handle the submit event and close the modal 2`] = ` Object { "loading": false, - "open": false, "userGroups": Object { "11": Object { "description": "Technical accounts", @@ -38,21 +37,6 @@ Object { } `; -exports[`should correctly handle user interactions 1`] = ` -Object { - "loading": false, - "open": true, - "userGroups": Object { - "11": Object { - "description": "Technical accounts", - "id": 11, - "name": "pull-request-analysers", - "selected": true, - }, - }, -} -`; - exports[`should correctly select the groups 1`] = ` Object { "11": Object { @@ -68,136 +52,48 @@ Object { } `; -exports[`should render and open the modal 1`] = ` - - organization.members.manage_groups - -`; - -exports[`should render and open the modal 2`] = ` -Array [ - - organization.members.manage_groups - , - + organization.members.manage_groups + + + -
    -

    - organization.members.manage_groups -

    -
    - + organization.members.members_groups.Admin Istrator + + + +
    +
    -
    - - organization.members.members_groups.Admin Istrator - - -
      + +
    + cancel +
    -
    -
    - - -
    -
    - - , -] -`; - -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, - }, - }, -} +
    + +
    `; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap index 861e8d98f02..eb235ae0e4a 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap @@ -1,77 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should correctly handle user interactions 1`] = ` -Array [ - Array [ - Object { - "avatar": "", - "groupCount": 3, - "login": "admin", - "name": "Admin Istrator", - }, - ], -] -`; - -exports[`should render and open the modal 1`] = ` - - organization.members.remove - -`; - -exports[`should render and open the modal 2`] = ` -Array [ - - organization.members.remove - , - + users.remove + + +
    -
    -

    - users.remove -

    -
    - +
    -
    -

    - organization.members.remove_x.Admin Istrator.MyOrg -

    +
    + +
    -
    -
    - - -
    -
    - - , -] +
    + +
    `; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css deleted file mode 100644 index fad51874d3d..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -.organization-switch { - display: inline-block; -} - -.organization-switch .dropdown-toggle { - display: flex; - align-items: center; - height: calc(4 * var(--gridSize)); - padding: 0 var(--gridSize); - border: 1px solid transparent; - border-radius: 2px; - box-sizing: border-box; - color: var(--baseFontColor) !important; - transition: all 0.3s ease; -} - -.organization-switch .dropdown-toggle:hover, -.organization-switch.open .dropdown-toggle { - border-color: var(--barBorderColor); - background-color: #fff; - box-shadow: var(--defaultShadow); -} - -.organization-switch.open .dropdown-toggle { - border-bottom-color: transparent; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.organization-switch .dropdown-menu { - min-width: 100%; - margin-left: -1px; -} - -.organization-switch .dropdown-menu > li > a { - padding-left: var(--gridSize); - padding-right: var(--gridSize); -} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx index 636dc6cf0a1..f9b1e0ddf81 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx @@ -24,7 +24,6 @@ import OrganizationNavigationMenu from './OrganizationNavigationMenu'; import * as theme from '../../../app/theme'; import ContextNavBar from '../../../components/nav/ContextNavBar'; import { Organization } from '../../../app/types'; -import './OrganizationNavigation.css'; interface Props { location: { pathname: string }; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx index 338494e7604..3b6f30e664f 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx @@ -48,71 +48,69 @@ export default function OrganizationNavigationAdministration({ location, organiz ); return ( - - {({ onToggleClick, open }) => ( -
  • - - {translate('layout.settings')} - - -
      - {extensions.map(extension => ( -
    • - - {extension.name} - -
    • - ))} -
    • - - {translate('user_groups.page')} - -
    • -
    • - - {translate('permissions.page')} - -
    • -
    • + + {extensions.map(extension => ( +
    • - {translate('permission_templates')} - -
    • -
    • - - {translate('projects_management')} - -
    • -
    • - - {translate('webhooks.page')} + activeClassName="active" + to={`/organizations/${organization.key}/extension/${extension.key}`}> + {extension.name}
    • + ))} +
    • + + {translate('user_groups.page')} + +
    • +
    • + + {translate('permissions.page')} + +
    • +
    • + + {translate('permission_templates')} + +
    • +
    • + + {translate('projects_management')} + +
    • +
    • + + {translate('webhooks.page')} + +
    • +
    • + + {translate('edit')} + +
    • + {organization.canDelete && (
    • - - {translate('edit')} + + {translate('delete')}
    • - {organization.canDelete && ( -
    • - - {translate('delete')} - -
    • - )} -
    -
  • - )} + )} + + } + tagName="li"> + + {translate('layout.settings')} + +
    ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx index 5151194ba9f..67eac961218 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx @@ -23,6 +23,7 @@ import * as classNames from 'classnames'; import { Organization } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; import Dropdown from '../../../components/controls/Dropdown'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; interface Props { location: { pathname: string }; @@ -40,31 +41,28 @@ export default function OrganizationNavigationExtensions({ location, organizatio ); return ( - - {({ onToggleClick, open }) => ( -
  • - - {translate('more')} - - - -
      - {extensions.map(extension => ( -
    • - - {extension.name} - -
    • - ))} -
    -
  • - )} + + {extensions.map(extension => ( +
  • + + {extension.name} + +
  • + ))} + + } + tagName="li"> + + {translate('more')} + +
    ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx index c6bc236126a..df497ccd8fc 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { sortBy } from 'lodash'; import { Organization } from '../../../app/types'; import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; @@ -38,20 +37,19 @@ export default function OrganizationNavigationHeader({ organization, organizatio
    {other.length ? ( - - {({ onToggleClick, open }) => ( -
    - - {organization.name} - - -
      - {sortBy(other, org => org.name.toLowerCase()).map(organization => ( - - ))} -
    -
    - )} + + {sortBy(other, org => org.name.toLowerCase()).map(organization => ( + + ))} + + }> + + {organization.name} + + ) : ( {organization.name} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx index f68fab3ac9f..eb8baeef190 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx @@ -33,5 +33,5 @@ it('renders', () => { }} /> ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx index f6afd014d72..57d43dc4682 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx @@ -52,5 +52,5 @@ it('renders dropdown', () => { organizations={organizations} /> ); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap index c7764bf1c8b..27d6f917f78 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap @@ -1,83 +1,84 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` -
  • +
  • + + user_groups.page + +
  • +
  • + + permissions.page + +
  • +
  • + + permission_templates + +
  • +
  • + + projects_management + +
  • +
  • + + webhooks.page + +
  • +
  • + + edit + +
  • + + } + tagName="li" > layout.settings -
      -
    • - - user_groups.page - -
    • -
    • - - permissions.page - -
    • -
    • - - permission_templates - -
    • -
    • - - projects_management - -
    • -
    • - - webhooks.page - -
    • -
    • - - edit - -
    • -
    - +
    `; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap index 949bb39ff04..626ef846617 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap @@ -22,44 +22,43 @@ exports[`renders 1`] = ` `; exports[`renders dropdown 1`] = ` -
    + + + + } > Foo -
      - - -
    -
    + `; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx index bdc74e4d941..855b3201c0c 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx @@ -22,58 +22,20 @@ import MetaTagsSelector from './MetaTagsSelector'; import { setProjectTags } from '../../../api/components'; import { translate } from '../../../helpers/l10n'; import TagsList from '../../../components/tags/TagsList'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { Component } from '../../../app/types'; import { Button } from '../../../components/ui/buttons'; +import Dropdown from '../../../components/controls/Dropdown'; +import { PopupPlacement } from '../../../components/ui/popups'; interface Props { component: Component; onComponentChange: (changes: {}) => void; } -interface State { - popupOpen: boolean; - popupPosition: BubblePopupPosition; -} - -export default class MetaTags extends React.PureComponent { +export default class MetaTags extends React.PureComponent { 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 { render() { const { key } = this.props.component; - const { popupOpen, popupPosition } = this.state; const tags = this.props.component.tags || []; if (this.canUpdateTags()) { return (
    (this.card = card)}> - - {popupOpen && ( -
    (this.tagsSelector = tagsSelector)}> + -
    - )} + } + overlayPlacement={PopupPlacement.BottomLeft}> + +
    ); } else { diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx index 783688ebf7c..fcc2952d745 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx @@ -20,11 +20,9 @@ import * as React from 'react'; import { without, difference } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; -import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { searchProjectTags } from '../../../api/components'; interface Props { - position: BubblePopupPosition; project: string; selectedTags: string[]; setProjectTags: (tags: string[]) => void; @@ -78,7 +76,6 @@ export default class MetaTagsSelector extends React.PureComponent onSearch={this.onSearch} onSelect={this.onSelect} onUnselect={this.onUnselect} - position={this.props.position} selectedTags={this.props.selectedTags} tags={availableTags} /> diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx index 7fa2003a331..b2ed2b1ac1e 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../../helpers/testUtils'; import MetaTags from '../MetaTags'; const component = { @@ -61,21 +60,3 @@ it('should render with tags and admin rights', () => { }) ).toMatchSnapshot(); }); - -it('should open the tag selector on click', () => { - const wrapper = shallow( - , - { - disableLifecycleMethods: true - } - ); - expect(wrapper).toMatchSnapshot(); - - // open - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); - - // close - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx index aefeb8d6fb6..0c4c24c9b1b 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx @@ -38,26 +38,14 @@ it('searches tags on mount', () => { (searchProjectTags as jest.Mock).mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] }) ); - mount( - - ); + mount(); expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); }); it('selects and deselects tags', () => { const setProjectTags = jest.fn(); const wrapper = shallow( - + ); const tagSelect: any = wrapper.find('TagsSelector'); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap index d0b063accf7..7eb0fedde2c 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap @@ -1,112 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should open the tag selector on click 1`] = ` -
    - -
    -`; - -exports[`should open the tag selector on click 2`] = ` -
    - -
    - -
    -
    -`; - -exports[`should open the tag selector on click 3`] = ` -
    - -
    -`; - exports[`should render with tags and admin rights 1`] = `
    - + +
    `; diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 0d0dc6dbee9..99b68c86254 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -389,15 +389,11 @@ border: none; } -.overview-analysis-graph .bubble-popup { +.overview-analysis-graph-popup { opacity: 0.8; padding: 0; } -.overview-analysis-graph .bubble-popup-arrow { - top: 7px; -} - .overview-analysis-graph-tooltip { padding: 4px; pointer-events: none; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx index 121c008ce9a..7ef344a3b23 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { difference } from 'lodash'; +import DeleteForm from './DeleteForm'; import Form from './Form'; import { setDefaultPermissionTemplate, @@ -28,11 +29,10 @@ import { } from '../../../api/permissions'; import { PermissionTemplate } from '../../../app/types'; import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; import QualifierIcon from '../../../components/shared/QualifierIcon'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; -interface Props { +export interface Props { fromDetails?: boolean; organization?: { isDefault?: boolean; key: string }; permissionTemplate: PermissionTemplate; @@ -41,6 +41,7 @@ interface Props { } interface State { + deleteForm: boolean; updateModal: boolean; } @@ -51,7 +52,7 @@ export default class ActionsCell extends React.PureComponent { 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 { ); }; - 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 { : '/permission_templates'; return ( - - {this.renderSetDefaultsControl()} - - {!this.props.fromDetails && ( - - {translate('edit_permissions')} + <> + + {this.renderSetDefaultsControl()} + + {!this.props.fromDetails && ( + + {translate('edit_permissions')} + + )} + + + {translate('update_details')} - )} - - {translate('update_details')} - + {t.defaultFor.length === 0 && ( + + {translate('delete')} + + )} + + {this.state.updateModal && (
    { /> )} - {t.defaultFor.length === 0 && ( - - {({ onClick }) => ( - - {translate('delete')} - - )} - + {this.state.deleteForm && ( + )} - + ); } } diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx new file mode 100644 index 00000000000..b89b76e1c31 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { PermissionTemplate } from '../../../app/types'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + onClose: () => void; + onSubmit: () => Promise; + permissionTemplate: PermissionTemplate; +} + +export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }: Props) { + const header = translate('permission_template.delete_confirm_title'); + + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( + +
    +

    {header}

    +
    + +
    + {translateWithParameters( + 'permission_template.do_you_want_to_delete_template_xxx', + t.name + )} +
    + +
    + + + {translate('delete')} + + + {translate('cancel')} + +
    + + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx similarity index 88% rename from server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js rename to server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx index b4c48de8bf8..a3ea017810c 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx @@ -17,23 +17,24 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; -import ActionsCell from '../ActionsCell'; +import ActionsCell, { Props } from '../ActionsCell'; const SAMPLE = { + createdAt: '2018-01-01', id: 'id', name: 'name', permissions: [], defaultFor: [] }; -function renderActionsCell(props) { +function renderActionsCell(props?: Partial) { return shallow( true} + topQualifiers={['TRK', 'VW']} {...props} /> ); @@ -53,7 +54,7 @@ it('should not set default', () => { }); it('should display all qualifiers for default organization', () => { - const organization = { isDefault: true }; + const organization = { isDefault: true, key: 'org' }; const setDefault = renderActionsCell({ organization }).find('.js-set-default'); expect(setDefault.length).toBe(2); expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK'); @@ -61,7 +62,7 @@ it('should display all qualifiers for default organization', () => { }); it('should display only projects for custom organization', () => { - const organization = { isDefault: false }; + const organization = { isDefault: false, key: 'org' }; const setDefault = renderActionsCell({ organization }).find('.js-set-default'); expect(setDefault.length).toBe(1); expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK'); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js index cf17cc3b037..ed7b41bb49b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js @@ -19,15 +19,14 @@ */ // @flow import React from 'react'; -import classNames from 'classnames'; import GraphsTooltipsContent from './GraphsTooltipsContent'; import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication'; import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues'; import { DEFAULT_GRAPH } from '../utils'; -import BubblePopup from '../../../components/common/BubblePopup'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import { Popup, PopupPlacement } from '../../../components/ui/popups'; /*:: import type { Event, MeasureHistory } from '../types'; */ /*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */ @@ -88,17 +87,18 @@ export default class GraphsTooltips extends React.PureComponent { const { events, measuresHistory, tooltipIdx } = this.props; const top = 30; let left = this.props.tooltipPos + 60; - let customClass; + let placement = PopupPlacement.RightTop; if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) { left -= TOOLTIP_WIDTH; - customClass = 'bubble-popup-right'; + placement = PopupPlacement.LeftTop; } const tooltipContent = this.renderContent().filter(Boolean); const addSeparator = tooltipContent.length > 0; return ( - +
    @@ -125,7 +125,7 @@ export default class GraphsTooltips extends React.PureComponent { )}
    - + ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index 5324153a4e0..d34e8de5448 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -25,7 +25,8 @@ import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; import ActionsDropdown, { - ActionsDropdownDivider + ActionsDropdownDivider, + ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis } from '../types'; */ @@ -45,15 +46,61 @@ type Props = { selected: boolean, updateSelectedDate: Date => void }; + +type State = { + addEventForm: bool, + addVersionForm: bool, + removeAnalysisForm: bool +} */ export default class ProjectActivityAnalysis extends React.PureComponent { + mounted /*: boolean */ = false; /*:: props: Props; */ + state /*: State */ = { addEventForm: false, addVersionForm: false, removeAnalysisForm: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } handleClick = () => this.props.updateSelectedDate(this.props.analysis.date); stopPropagation = (e /*: Event */) => e.stopPropagation(); + handleRemoveAnalysisClick = () => { + this.setState({ removeAnalysisForm: true }); + }; + + closeRemoveAnalysisForm = () => { + if (this.mounted) { + this.setState({ removeAnalysisForm: false }); + } + }; + + handleAddEventClick = () => { + this.setState({ addEventForm: true }); + }; + + closeAddEventForm = () => { + if (this.mounted) { + this.setState({ addEventForm: false }); + } + }; + + handleAddVersionClick = () => { + this.setState({ addVersionForm: true }); + }; + + closeAddVersionForm = () => { + if (this.mounted) { + this.setState({ addVersionForm: false }); + } + }; + render() { const { analysis, isFirst, canAdmin } = this.props; const { date, events } = analysis; @@ -71,7 +118,6 @@ export default class ProjectActivityAnalysis extends React.PureComponent { })} data-date={date.valueOf()} onClick={this.handleClick} - role="listitem" tabIndex="0">
    @@ -80,29 +126,53 @@ export default class ProjectActivityAnalysis extends React.PureComponent { {(canAddVersion || canAddEvent || canDeleteAnalyses) && (
    - + {canAddVersion && ( - + + {translate('project_activity.add_version')} + )} {canAddEvent && ( - + + {translate('project_activity.add_custom_event')} + )} {(canAddVersion || canAddEvent) && canDeleteAnalyses && } {canDeleteAnalyses && ( - + + {translate('project_activity.delete_analysis')} + )} + + {this.state.addVersionForm && ( + + )} + + {this.state.addEventForm && ( + + )} + + {this.state.removeAnalysisForm && ( + + )}
    )} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap index c2723826cfe..3089427f9ac 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap @@ -1,9 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should not add separators if not needed 1`] = ` -
    - + `; exports[`should render correctly for issues graphs 1`] = ` -
    -
    + `; exports[`should render correctly for random graphs 1`] = ` -
    - + `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js index b47c72d926b..ded7c2b733c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate } from '../../../../helpers/l10n'; /*:: import type { Analysis } from '../../types'; */ @@ -28,13 +27,13 @@ import { translate } from '../../../../helpers/l10n'; type Props = { addEvent: (analysis: string, name: string, category?: string) => Promise<*>, analysis: Analysis, - addEventButtonText: string + addEventButtonText: string, + onClose: () => void; }; */ /*:: type State = { - open: boolean, processing: boolean, name: string }; @@ -44,7 +43,6 @@ export default class AddEventForm extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { - open: false, processing: false, name: '' }; @@ -57,16 +55,6 @@ export default class AddEventForm extends React.PureComponent { this.mounted = false; } - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - if (this.mounted) { - this.setState({ open: false, name: '' }); - } - }; - changeInput = (e /*: Object */) => { if (this.mounted) { this.setState({ name: e.target.value }); @@ -79,24 +67,18 @@ export default class AddEventForm extends React.PureComponent { } }; - stopProcessingAndClose = () => { - if (this.mounted) { - this.setState({ open: false, processing: false, name: '' }); - } - }; - handleSubmit = (e /*: Object */) => { e.preventDefault(); this.setState({ processing: true }); this.props .addEvent(this.props.analysis.key, this.state.name) - .then(this.stopProcessingAndClose, this.stopProcessing); + .then(this.props.onClose, this.stopProcessing); }; - renderModal() { + render() { const header = translate(this.props.addEventButtonText); return ( - +

    {header}

    @@ -106,11 +88,11 @@ export default class AddEventForm extends React.PureComponent {
    @@ -121,7 +103,7 @@ export default class AddEventForm extends React.PureComponent { ) : (
    -
    @@ -131,16 +113,4 @@ export default class AddEventForm extends React.PureComponent { ); } - - render() { - const linkComponent = ( - - {translate(this.props.addEventButtonText)} - - ); - if (this.state.open) { - return [linkComponent, this.renderModal()]; - } - return linkComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js index 78b47814b1b..3bf12702367 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js @@ -22,7 +22,8 @@ import React from 'react'; import { find, sortBy } from 'lodash'; import AddGraphMetricPopup from './AddGraphMetricPopup'; import DropdownIcon from '../../../../components/icons-components/DropdownIcon'; -import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; +import Dropdown from '../../../../components/controls/Dropdown'; +import { Button } from '../../../../components/ui/buttons'; import { isDiffMetric } from '../../../../helpers/measures'; import { getLocalizedMetricName, translate } from '../../../../helpers/l10n'; /*:: import type { Metric } from '../../types'; */ @@ -40,7 +41,6 @@ type Props = { /*:: type State = { - open: boolean, query: string, }; */ @@ -48,7 +48,6 @@ type State = { export default class AddGraphMetric extends React.PureComponent { /*:: props: Props; */ state /*: State */ = { - open: false, metrics: [], query: '', selectedMetrics: [] @@ -107,12 +106,6 @@ export default class AddGraphMetric extends React.PureComponent { return metric === undefined ? key : getLocalizedMetricName(metric); }; - toggleForm = () => { - this.setState(state => { - return { open: !state.open }; - }); - }; - onSearch = (query /*: string */) => { this.setState({ query }); return Promise.resolve(); @@ -138,10 +131,6 @@ export default class AddGraphMetric extends React.PureComponent { }); }; - togglePopup = (open /*: boolean*/) => { - this.setState({ open }); - }; - render() { const { query } = this.state; const filteredMetrics = this.filterMetricsElements(this.props, query); @@ -151,34 +140,27 @@ export default class AddGraphMetric extends React.PureComponent { query ); return ( -
    - this.getLocalizedMetricNameFromKey(element)} - selectedElements={selectedMetrics} - /> - } - position="bottomright" - togglePopup={this.togglePopup}> - - -
    + this.getLocalizedMetricNameFromKey(element)} + selectedElements={selectedMetrics} + /> + }> + + ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx index 81864bf8920..d4bf8bbd2e7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BubblePopup from '../../../../components/common/BubblePopup'; import MultiSelect from '../../../../components/common/MultiSelect'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; @@ -58,9 +57,7 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr } return ( - +
    - +
    ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js index 47125ed09fe..e813985139a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js @@ -20,20 +20,19 @@ // @flow import React from 'react'; import Modal from '../../../../components/controls/Modal'; -import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { translate } from '../../../../helpers/l10n'; /*:: import type { Analysis } from '../../types'; */ /*:: type Props = { analysis: Analysis, - deleteAnalysis: (analysis: string) => Promise<*> + deleteAnalysis: (analysis: string) => Promise<*>, + onClose: () => void; }; */ /*:: type State = { - open: boolean, processing: boolean }; */ @@ -42,7 +41,6 @@ export default class RemoveAnalysisForm extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { - open: false, processing: false }; @@ -54,40 +52,24 @@ export default class RemoveAnalysisForm extends React.PureComponent { this.mounted = false; } - openForm = () => { - this.setState({ open: true }); - }; - - closeForm = () => { - if (this.mounted) { - this.setState({ open: false }); - } - }; - stopProcessing = () => { if (this.mounted) { this.setState({ processing: false }); } }; - stopProcessingAndClose = () => { - if (this.mounted) { - this.setState({ open: false, processing: false }); - } - }; - handleSubmit = (e /*: Event */) => { e.preventDefault(); this.setState({ processing: true }); this.props .deleteAnalysis(this.props.analysis.key) - .then(this.stopProcessingAndClose, this.stopProcessing); + .then(this.props.onClose, this.stopProcessing); }; - renderModal() { + render() { const header = translate('project_activity.delete_analysis'); return ( - +

    {header}

    @@ -100,10 +82,10 @@ export default class RemoveAnalysisForm extends React.PureComponent { ) : (
    - -
    @@ -113,19 +95,4 @@ export default class RemoveAnalysisForm extends React.PureComponent {
    ); } - - render() { - const linkComponent = ( - - {translate('project_activity.delete_analysis')} - - ); - if (this.state.open) { - return [linkComponent, this.renderModal()]; - } - return linkComponent; - } } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx index 6298f16e0a3..f0ec4ed6f3c 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx @@ -153,15 +153,17 @@ export default class App extends React.PureComponent { return ( {showOrphanHeader && ( -
  • -
    - {translate('branches.orphan_branches')} -
    - -
  • + + +
    + {translate('branches.orphan_branches')} +
    + + + )} -
  • -
    + - branches.orphan_branches -
    - -
  • +
    + branches.orphan_branches +
    + + + { {translate('my_account.analyze_new_project')} - - {({ onToggleClick, open }) => ( -
    - - {translate('projects.no_favorite_projects.favorite_projects_from_orgs')} - - -
      - {sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( - - ))} -
    -
    - )} + + {sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + + ))} + + }> + + {translate('projects.no_favorite_projects.favorite_projects_from_orgs')} + + {translate('projects.no_favorite_projects.favorite_public_projects')} @@ -90,7 +85,7 @@ export class NoFavoriteProjects extends React.PureComponent { {translate('projects.no_favorite_projects.engagement')}

    - + {translate('projects.explore_projects')}

    diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap index a7dede57348..8cf6f203496 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap @@ -52,7 +52,45 @@ exports[`renders for SonarCloud 1`] = ` > my_account.analyze_new_project - + + + + + } + > + + projects.no_favorite_projects.favorite_projects_from_orgs + + + const { hasAccess } = this.state; return ( - - {hasAccess === true && ( - - {translate('edit_permissions')} - - )} + <> + + {hasAccess === true && ( + + {translate('edit_permissions')} + + )} + + {hasAccess === false && ( + + {translate('global_permissions.restore_access')} + + )} - {hasAccess === false && ( - {translate('global_permissions.restore_access')} + className="js-apply-template" + onClick={this.handleApplyTemplateClick}> + {translate('projects_role.apply_template')} - )} - - - {translate('projects_role.apply_template')} - + {this.state.restoreAccessModal && ( project={this.props.project} /> )} - + ); } } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx index b30e553d91d..df159be98da 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx @@ -44,7 +44,7 @@ it('restores access', async () => { const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); - wrapper.prop('onOpen')(); + wrapper.find('ActionsDropdown').prop('onOpen')(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap index 61ea2fee3de..cb6c9944857 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap @@ -18,53 +18,59 @@ exports[`applies permission template 1`] = ` `; exports[`restores access 1`] = ` - - + - projects_role.apply_template - - + + projects_role.apply_template + + +
    `; exports[`restores access 2`] = ` - - + - global_permissions.restore_access - - - projects_role.apply_template - - + + global_permissions.restore_access + + + projects_role.apply_template + + +
    `; exports[`restores access 3`] = ` - - - global_permissions.restore_access - - + - projects_role.apply_template - + + global_permissions.restore_access + + + projects_role.apply_template + + - +
    `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 018d35320f7..17dea8322e9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -138,58 +138,60 @@ export default class ProfileActions extends React.PureComponent { ); return ( - - {actions.edit && ( - - {translate('quality_profiles.activate_more_rules')} - - )} - - {!profile.isBuiltIn && ( - - {translate('backup_verb')} - - )} - - - {translate('compare')} - - - {actions.copy && ( - - {translate('copy')} - - )} + <> + + {actions.edit && ( + + {translate('quality_profiles.activate_more_rules')} + + )} + + {!profile.isBuiltIn && ( + + {translate('backup_verb')} + + )} - {actions.edit && ( - - {translate('rename')} - - )} - - {actions.setAsDefault && ( - {translate('set_as_default')} + id="quality-profile-compare" + to={getProfileComparePath(profile.name, profile.language, this.props.organization)}> + {translate('compare')} - )} - {actions.delete && } - - {actions.delete && ( - - {translate('delete')} - - )} + {actions.copy && ( + + {translate('copy')} + + )} + + {actions.edit && ( + + {translate('rename')} + + )} + + {actions.setAsDefault && ( + + {translate('set_as_default')} + + )} + + {actions.delete && } + + {actions.delete && ( + + {translate('delete')} + + )} + {this.state.copyFormOpen && ( { profile={profile} /> )} - + ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index 01f38890790..86a903e0bef 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -1,139 +1,145 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders with all permissions 1`] = ` - - + + - quality_profiles.activate_more_rules - - - backup_verb - - + quality_profiles.activate_more_rules + + + backup_verb + + - compare - - - copy - - - rename - - - set_as_default - - - - delete - - + > + compare + + + copy + + + rename + + + set_as_default + + + + delete + + + `; exports[`renders with no permissions 1`] = ` - - - backup_verb - - + + + backup_verb + + - compare - - + > + compare + + + `; exports[`renders with permission to edit only 1`] = ` - - + + - quality_profiles.activate_more_rules - - - backup_verb - - + quality_profiles.activate_more_rules + + + backup_verb + + - compare - - - rename - - + > + compare + + + rename + + + `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx index 80ef81fbb54..8a0750085a4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { IndexLink } from 'react-router'; -import * as classNames from 'classnames'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getProfilesPath, getProfilesForLanguagePath } from '../utils'; import Dropdown from '../../../components/controls/Dropdown'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; interface Props { currentFilter?: string; @@ -48,36 +48,31 @@ export default function ProfilesListHeader({ currentFilter, languages, organizat return (
    - - {({ onToggleClick, open }) => ( -
    - - {label} - - - -
      -
    • - - {translate('quality_profiles.all_profiles')} + +
    • + + {translate('quality_profiles.all_profiles')} + +
    • + {languages.map(language => ( +
    • + + {language.name}
    • - {languages.map(language => ( -
    • - - {language.name} - -
    • - ))} -
    -
    - )} + ))} + + }> + + {label} + +
    ); diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index e34bbf7e10e..dd4c07d9b84 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import ChangeLogLevelForm from './ChangeLogLevelForm'; import RestartForm from '../../../components/common/RestartForm'; import { getFileNameSuffix } from '../utils'; @@ -102,53 +101,52 @@ export default class PageActions extends React.PureComponent { />
    {this.props.canDownloadLogs && ( - - {({ onToggleClick, open }) => ( -
    - - -
    - )} + +
  • + + Main Process + +
  • +
  • + + Compute Engine + +
  • +
  • + + Search Engine + +
  • +
  • + + Web Server + +
  • + + }> +
    )} ({ it('should render correctly', () => { const wrapper = getWrapper({ serverId: 'MyServerId' }); expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('should render without restart and log download', () => { diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap index 3c698c9ba19..14a070daf60 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap @@ -22,7 +22,62 @@ exports[`should render correctly 1`] = ` onClick={[Function]} />
    - + +
  • + + Main Process + +
  • +
  • + + Compute Engine + +
  • +
  • + + Search Engine + +
  • +
  • + + Web Server + +
  • + + } + > + +
    +
  • + + Main Process + +
  • +
  • + + Compute Engine + +
  • +
  • + + Search Engine + +
  • +
  • + + Web Server + +
  • + + } > - - - + `; exports[`should render without restart and log download 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx index ccd1471a245..5c132436de1 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx @@ -49,7 +49,7 @@ export default class UserActions extends React.PureComponent { renderActions = () => { const { user } = this.props; return ( - + {translate('update_details')} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap index 0fd31e7f6ae..9b8a3267c76 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap @@ -2,9 +2,7 @@ exports[`should render correctly 1`] = ` - + void; + onSubmit: () => Promise; + webhook: Webhook; +} + +export default function DeleteWebhookForm({ onClose, onSubmit, webhook }: Props) { + const header = translate('webhooks.delete'); + + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( +
    +
    +

    {header}

    +
    + +
    + {translateWithParameters('webhooks.delete.confirm', webhook.name)} +
    + +
    + + + {translate('delete')} + + + {translate('cancel')} + +
    +
    + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx index 0027720ad60..cfbeff267e3 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import CreateWebhookForm from './CreateWebhookForm'; +import DeleteWebhookForm from './DeleteWebhookForm'; import DeliveriesForm from './DeliveriesForm'; import ActionsDropdown, { ActionsDropdownItem, ActionsDropdownDivider } from '../../../components/controls/ActionsDropdown'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { Webhook } from '../../../app/types'; interface Props { @@ -35,13 +35,14 @@ interface Props { } interface State { + deleting: boolean; deliveries: boolean; updating: boolean; } export default class WebhookActions extends React.PureComponent { 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 { 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 {
    )} - - {({ onClick }) => ( - - {translate('delete')} - - )} - + + {translate('delete')} +
    + {this.state.deliveries && ( )} + {this.state.updating && ( { webhook={webhook} /> )} + + {this.state.deleting && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx index 70fe2bc5c77..8e1fd5d57f4 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx @@ -55,14 +55,9 @@ it('should display the update webhook form', () => { it('should display the delete webhook form', () => { const onDelete = jest.fn(() => Promise.resolve()); const wrapper = getWrapper({ onDelete }); - click( - wrapper - .find('ConfirmButton') - .dive() - .find('.js-webhook-delete') - ); - expect(wrapper.find('ConfirmButton').exists()).toBeTruthy(); - wrapper.find('ConfirmButton').prop('onConfirm')(); + click(wrapper.find('.js-webhook-delete')); + expect(wrapper.find('DeleteWebhookForm').exists()).toBeTruthy(); + wrapper.find('DeleteWebhookForm').prop('onSubmit')(); expect(onDelete).lastCalledWith(webhook.key); }); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap index 7079ff776af..f72bc992de3 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap @@ -18,13 +18,13 @@ exports[`should display the deliveries form 1`] = ` webhooks.deliveries.show - + + delete +
    `; @@ -41,13 +41,13 @@ exports[`should render correctly 1`] = ` update_verb - + + delete +
    `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index 041856e2426..1499a05acb0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -21,7 +21,6 @@ import { stringify } from 'querystring'; import * as React from 'react'; import { Link } from 'react-router'; import * as PropTypes from 'prop-types'; -import * as classNames from 'classnames'; import MeasuresOverlay from './components/MeasuresOverlay'; import { SourceViewerFile, BranchLike } from '../../app/types'; import QualifierIcon from '../shared/QualifierIcon'; @@ -29,6 +28,7 @@ import Dropdown from '../controls/Dropdown'; import FavoriteContainer from '../controls/FavoriteContainer'; import ListIcon from '../icons-components/ListIcon'; import { ButtonIcon } from '../ui/buttons'; +import { PopupPlacement } from '../ui/popups'; import { WorkspaceContext } from '../workspace/context'; import { getPathUrlAsString, @@ -128,56 +128,54 @@ export default class SourceViewerHeader extends React.PureComponent - - {({ onToggleClick, open }) => ( - - )} + )} +
  • + + {translate('component_viewer.show_raw_source')} + +
  • + + } + overlayPlacement={PopupPlacement.BottomRight}> + + +
    + {this.state.measuresOverlay && ( + + )} +
    {isUnitTest && (
    diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx index 8e5aeca1727..618e258da8b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx @@ -22,8 +22,9 @@ import { groupBy } from 'lodash'; import * as PropTypes from 'prop-types'; import { getTests } from '../../../api/components'; import { BranchLike, SourceLine, TestCase } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import TestStatusIcon from '../../shared/TestStatusIcon'; +import { PopupPlacement } from '../../ui/popups'; import { WorkspaceContext } from '../../workspace/context'; import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; @@ -34,7 +35,6 @@ interface Props { componentKey: string; line: SourceLine; onClose: () => void; - popupPosition?: any; } interface State { @@ -114,53 +114,55 @@ export default class CoveragePopup extends React.PureComponent { }); return ( - -
    - {translate('source_viewer.covered')} - {!!line.conditions && ( -
    - {'('} - {line.coveredConditions || '0'} - {' of '} - {line.conditions} {translate('source_viewer.conditions')} - {')'} -
    + +
    +
    + {translate('source_viewer.covered')} + {!!line.conditions && ( +
    + {'('} + {line.coveredConditions || '0'} + {' of '} + {line.conditions} {translate('source_viewer.conditions')} + {')'} +
    + )} +
    + {this.state.loading ? ( + + ) : ( + <> + {testFiles.length === 0 && + translate('source_viewer.tooltip.no_information_about_tests')} + {testFiles.map(testFile => ( +
    + + {collapsePath(testFile.file.longName)} + +
      + {testFile.tests.map(testCase => ( +
    • + +
      {testCase.name}
      + {testCase.status !== 'SKIPPED' && ( + {testCase.durationInMs}ms + )} +
    • + ))} +
    +
    + ))} + )}
    - {this.state.loading ? ( - - ) : ( - <> - {testFiles.length === 0 && - translate('source_viewer.tooltip.no_information_about_tests')} - {testFiles.map(testFile => ( -
    - - {collapsePath(testFile.file.longName)} - -
      - {testFile.tests.map(testCase => ( -
    • - - {testCase.name} - {testCase.status !== 'SKIPPED' && ( - {testCase.durationInMs}ms - )} -
    • - ))} -
    -
    - ))} - - )} - +
    ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx index 163648dd6e2..1322545356f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -22,8 +22,9 @@ import { Link } from 'react-router'; import * as PropTypes from 'prop-types'; import { groupBy, sortBy } from 'lodash'; import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import QualifierIcon from '../../shared/QualifierIcon'; +import { PopupPlacement } from '../../ui/popups'; import { WorkspaceContext } from '../../workspace/context'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; @@ -90,8 +91,8 @@ export default class DuplicationPopup extends React.PureComponent { ); return ( - -
    + +
    {this.props.inRemovedComponent && (
    {translate('duplications.dups_found_on_deleted_resource')} @@ -99,11 +100,11 @@ export default class DuplicationPopup extends React.PureComponent { )} {duplications.length > 0 && ( <> -
    +
    {translate('component_viewer.transition.duplication')} -
    + {duplications.map(duplication => ( -
    +
    {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( <> @@ -164,7 +165,7 @@ export default class DuplicationPopup extends React.PureComponent { )}
    - + ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx index 17d98a03eca..be4c2d1a842 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import CoveragePopup from './CoveragePopup'; import { BranchLike, SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; +import Toggler from '../../controls/Toggler'; import { translate } from '../../../helpers/l10n'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { branchLike: BranchLike | undefined; @@ -59,7 +59,9 @@ export default class LineCoverage extends React.PureComponent { line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; const cell = line.coverageStatus ? ( - +
    ) : ( @@ -75,20 +77,19 @@ export default class LineCoverage extends React.PureComponent { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - {cell} - - } - position="bottomright" - togglePopup={this.handleTogglePopup} - /> + }> + {cell} + ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx index 0121bfa868d..93772dcbb72 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; +import Toggler from '../../controls/Toggler'; import { translate } from '../../../helpers/l10n'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { duplicated: boolean; @@ -54,6 +54,10 @@ export default class LineDuplicationBlock extends React.PureComponent { }); }; + 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 { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - - {cell} - - + + + {cell} + + ) : ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx index 9297036d88d..15af9ac258d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import LineOptionsPopup from './LineOptionsPopup'; import { BranchLike, SourceLine } from '../../../app/types'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; +import Toggler from '../../controls/Toggler'; interface Props { branchLike: BranchLike | undefined; @@ -42,6 +42,10 @@ export default class LineNumber extends React.PureComponent { 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 { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - } - position="bottomright" - togglePopup={this.handleTogglePopup} /> ) : ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx index 340304bb280..34a136d8391 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx @@ -20,7 +20,8 @@ import * as React from 'react'; import { Link } from 'react-router'; import { BranchLike, SourceLine } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; +import { PopupPlacement } from '../../ui/popups'; import { translate } from '../../../helpers/l10n'; import { getBranchLikeQuery } from '../../../helpers/branches'; @@ -28,21 +29,20 @@ interface Props { branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; - popupPosition?: any; } -export default function LineOptionsPopup({ branchLike, componentKey, line, popupPosition }: Props) { +export default function LineOptionsPopup({ branchLike, componentKey, line }: Props) { const permalink = { pathname: '/component', query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) } }; return ( - -
    + +
    {translate('component_viewer.get_permalink')}
    - +
    ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx index e692a3c46f1..83c326a12ea 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import SCMPopup from './SCMPopup'; import { SourceLine } from '../../../app/types'; -import BubblePopupHelper from '../../common/BubblePopupHelper'; +import Toggler from '../../controls/Toggler'; interface Props { line: SourceLine; @@ -41,6 +41,10 @@ export default class LineSCM extends React.PureComponent { 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 { // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" tabIndex={0}> - {cell} - } - position="bottomright" - togglePopup={this.handleTogglePopup} - /> + }> + {cell} + ) : ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx index f89a9aa408b..1e46274b9e9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx @@ -81,12 +81,12 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent -
    +
    {translate('component_viewer.transition.covers')} -
    + {coveredFiles.length > 0 ? coveredFiles.map(coveredFile => ( -
    +
    {coveredFile.longName} {translateWithParameters( @@ -102,7 +102,7 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent -
    {translate('component_viewer.details')}
    +
    {translate('component_viewer.details')}
    {testCase.message &&
    {testCase.message}
    }
    {testCase.stacktrace}
    diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx index 1a94d48e657..fa1b766c12a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx @@ -19,24 +19,26 @@ */ import * as React from 'react'; import { SourceLine } from '../../../app/types'; -import BubblePopup from '../../common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; import DateFormatter from '../../intl/DateFormatter'; +import { PopupPlacement } from '../../ui/popups'; interface Props { line: SourceLine; - popupPosition?: any; } -export default function SCMPopup({ line, popupPosition }: Props) { +export default function SCMPopup({ line }: Props) { return ( - -
    {line.scmAuthor}
    - {line.scmDate && ( -
    - -
    - )} - {line.scmRevision &&
    {line.scmRevision}
    } -
    + +
    +
    {line.scmAuthor}
    + {line.scmDate && ( +
    + +
    + )} + {line.scmRevision &&
    {line.scmRevision}
    } +
    +
    ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap index 4e01802b363..e715b7ded52 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap @@ -8,17 +8,10 @@ exports[`render covered line 1`] = ` role="button" tabIndex={0} > - -
    - - } - position="bottomright" - togglePopup={[Function]} - /> + > + +
    + + `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap index 9ea77716f15..7147eca868e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap @@ -9,19 +9,19 @@ exports[`render duplicated line 1`] = ` role="button" tabIndex={0} > - -
    - - + +
    + + `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap index 3841048e4ae..f5d3c9deff3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap @@ -14,15 +14,10 @@ exports[`render line 3 1`] = ` role="button" tabIndex={0} > - } - position="bottomright" - togglePopup={[Function]} /> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap index 13d7b6ee859..96b360bb80a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -
    -
    + `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap index c320b6a9b36..dac002460eb 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap @@ -8,15 +8,10 @@ exports[`does not render scm details 1`] = ` role="button" tabIndex={0} > - } - position="bottomright" - togglePopup={[Function]} /> `; @@ -41,19 +34,10 @@ exports[`render scm details 1`] = ` role="button" tabIndex={0} > -
    - } - position="bottomright" - togglePopup={[Function]} - /> + > +
    + `; @@ -78,19 +65,10 @@ exports[`render scm details for the first line 1`] = ` role="button" tabIndex={0} > -
    - } - position="bottomright" - togglePopup={[Function]} - /> + > +
    + `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap index 43ac2de8f8e..b053a1bf9af 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap @@ -12,11 +12,11 @@ exports[`should render ERROR test 1`] = ` className="source-viewer-measures-card source-viewer-measures-card-fixed-height" > -
    component_viewer.details -
    +
               Something failed
             
    @@ -39,13 +39,13 @@ exports[`should render OK test 1`] = ` className="source-viewer-measures-card source-viewer-measures-card-fixed-height" > -
    component_viewer.transition.covers -
    +
    - foo +
    + foo +
    +
    + +
    -
    - -
    - + `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css index 60a57ab69ba..839838acfe9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -396,10 +396,6 @@ clear: both; } -.source-viewer-measures .bubble-popup-section { - width: 100%; -} - .source-viewer-measures + .source-viewer-measures { margin-top: 40px; } @@ -508,9 +504,6 @@ } .source-viewer-bubble-popup { - top: -16px; - left: 100%; - width: 480px; font-family: var(--baseFontFamily); font-size: var(--baseFontSize); text-align: left; diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js index 2723b9dad7f..f4d54a0aa5f 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -251,24 +251,15 @@ export default class AdvancedTimeline extends React.PureComponent { handleMouseOut = (evt /*: Event & { relatedTarget: HTMLElement } */) => { const { updateTooltip } = this.props; - const targetClass = - evt.relatedTarget && typeof evt.relatedTarget.className === 'string' - ? evt.relatedTarget.className - : ''; - if ( - !updateTooltip || - targetClass.includes('bubble-popup') || - targetClass.includes('graph-tooltip') - ) { - return; + if (updateTooltip) { + this.setState({ + mouseOver: false, + selectedDate: null, + selectedDateXPos: null, + selectedDateIdx: null + }); + updateTooltip(null, null, null); } - this.setState({ - mouseOver: false, - selectedDate: null, - selectedDateXPos: null, - selectedDateIdx: null - }); - updateTooltip(null, null, null); }; handleClick = () => { diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx deleted file mode 100644 index 9f66d022cc9..00000000000 --- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import * as classNames from 'classnames'; -import { BubblePopupPosition } from './BubblePopup'; - -interface Props { - className?: string; - children?: React.ReactNode; - isOpen: boolean; - offset?: { vertical: number; horizontal: number }; - popup: JSX.Element; - position: 'bottomleft' | 'bottomright'; - togglePopup: (show: boolean) => void; -} - -interface State { - position: BubblePopupPosition; -} - -export default class BubblePopupHelper extends React.PureComponent { - 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) { - 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 ( -
    (this.container = container)} - role="tooltip" - tabIndex={0}> - {this.props.children} - {this.props.isOpen && ( -
    (this.popupContainer = popupContainer)}> - {React.cloneElement(this.props.popup, { - popupPosition: this.state.position - })} -
    - )} -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js deleted file mode 100644 index ffd00e6cb84..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow, mount } from 'enzyme'; -import React from 'react'; -import BubblePopupHelper from '../BubblePopupHelper'; -import BubblePopup from '../BubblePopup'; -import { click } from '../../../helpers/testUtils'; - -it('should render an open popup on the right', () => { - const toggle = jest.fn(); - const popup = shallow( - - test - - }> - - , - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render the popup helper with a closed popup', () => { - const toggle = jest.fn(); - const popup = shallow( - - test - - }> - - , - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render with custom classes', () => { - const toggle = jest.fn(); - const popup = shallow( - - test - - }> - - , - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); -}); - -it('should render the popup with offset', () => { - const toggle = jest.fn(); - const popup = mount( - - test - - }> - - - ); - expect(popup.find('BubblePopup')).toMatchSnapshot(); -}); - -it('should render an open popup on the left', () => { - const toggle = jest.fn(); - const popup = mount( - - test - - }> - - - ); - 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( - - test - - }> - - , - { disableLifecycleMethods: true } - ); - expect(popup).toMatchSnapshot(); - click(popup.find('button')); - expect(toggle.mock.calls.length).toBe(1); - expect(popup).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap deleted file mode 100644 index 08a7cb8825e..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render popup 1`] = ` -
    - - test - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap deleted file mode 100644 index 51f8e93fda5..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap +++ /dev/null @@ -1,182 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should correctly handle clicks on the button 1`] = ` -
    - -
    -`; - -exports[`should correctly handle clicks on the button 2`] = ` -
    - -
    - - - test - - -
    -
    -`; - -exports[`should render an open popup on the left 1`] = ` - -
    - - test - -
    -
    - -`; - -exports[`should render an open popup on the right 1`] = ` -
    - -
    - - - test - - -
    -
    -`; - -exports[`should render the popup helper with a closed popup 1`] = ` -
    - -
    -`; - -exports[`should render the popup with offset 1`] = ` - -
    - - test - -
    -
    - -`; - -exports[`should render with custom classes 1`] = ` -
    - -
    - - - test - - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx index f1173c88e75..252d26ef618 100644 --- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -28,34 +28,24 @@ import { Button } from '../ui/buttons'; interface Props { className?: string; children: React.ReactNode; - menuClassName?: string; - menuPosition?: 'left' | 'right'; onOpen?: () => void; small?: boolean; toggleClassName?: string; } -export default function ActionsDropdown({ menuPosition = 'right', ...props }: Props) { +export default function ActionsDropdown(props: Props) { return ( - - {({ onToggleClick, open }) => ( -
    - -
      - {props.children} -
    -
    - )} + {props.children}}> + ); } diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index e5794463a32..886a43211e4 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -151,73 +151,71 @@ export default class DateInput extends React.PureComponent { return ( - {({ ref }) => ( - - (this.input = node)} - type="text" - value={formattedValue || ''} - /> - - {this.props.value !== undefined && ( - - - - )} - {this.state.open && ( -
    - - } - disabledDays={{ after, before: minDate }} - firstDayOfWeek={1} - modifiers={modifiers} - month={this.state.currentMonth} - navbarElement={} - onDayClick={this.handleDayClick} - onDayMouseEnter={this.handleDayMouseEnter} - selectedDays={selectedDays} - weekdaysLong={weekdaysLong} - weekdaysShort={weekdaysShort} - /> -
    - )} -
    - )} + + (this.input = node)} + type="text" + value={formattedValue || ''} + /> + + {this.props.value !== undefined && ( + + + + )} + {this.state.open && ( +
    + + } + disabledDays={{ after, before: minDate }} + firstDayOfWeek={1} + modifiers={modifiers} + month={this.state.currentMonth} + navbarElement={} + onDayClick={this.handleDayClick} + onDayMouseEnter={this.handleDayMouseEnter} + selectedDays={selectedDays} + weekdaysLong={weekdaysLong} + weekdaysShort={weekdaysShort} + /> +
    + )} +
    ); } diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx similarity index 61% rename from server/sonar-web/src/main/js/components/common/BubblePopup.tsx rename to server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx index e23a99b7e25..9c13423e454 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx +++ b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx @@ -18,32 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; - -export interface BubblePopupPosition { - top?: number; - left?: number; - right?: number; -} interface Props { - customClass?: string; children: React.ReactNode; - position?: BubblePopupPosition; + onClick: () => void; } -/** - * Deprecated. - * Use instead. - */ -export default function BubblePopup(props: Props) { - const popupClass = classNames('bubble-popup', props.customClass); - const popupStyle = { ...props.position }; +export default class DocumentClickHandler extends React.Component { + componentDidMount() { + setTimeout(() => { + this.addClickHandler(); + }, 0); + } + + componentWillUnmount() { + this.removeClickHandler(); + } + + addClickHandler = () => { + document.addEventListener('click', this.handleDocumentClick); + }; + + removeClickHandler = () => { + document.removeEventListener('click', this.handleDocumentClick); + }; + + handleDocumentClick = () => { + this.props.onClick(); + }; - return ( -
    - {props.children} -
    -
    - ); + render() { + return this.props.children; + } } diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx index 3b82fd1c408..a90414a235c 100644 --- a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx @@ -18,16 +18,33 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; +import ScreenPositionFixer from './ScreenPositionFixer'; +import Toggler from './Toggler'; +import { Popup, PopupPlacement } from '../ui/popups'; + +interface OnClickCallback { + (event?: React.SyntheticEvent): void; +} interface RenderProps { closeDropdown: () => void; - onToggleClick: (event?: React.SyntheticEvent) => 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 { - 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) => { if (event) { - this.toggleNode = event.currentTarget; event.preventDefault(); event.currentTarget.blur(); } @@ -85,10 +71,89 @@ export default class Dropdown extends React.PureComponent { }; 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 = ( + + {this.props.overlay} + + }> + {child} + + ); + + 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 - - + + + + + `; -exports[`should render 3`] = ` - - - - - - - + + + + + + `; -exports[`should render 4`] = ` - - - - - - -
    - - } - 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={} - 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", - ] - } - /> -
    -
    + firstDayOfWeek={1} + month={2018-01-17T00:00:00.000Z} + navbarElement={} + 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", + ] + } + /> +
    + + `; exports[`should select a day 1`] = ` - - - - + + + + + `; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap new file mode 100644 index 00000000000..a782ff8e6f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render click wrappers 1`] = ` + +
    +
    + +`; + +exports[`should render children and overlay 1`] = ` + +
    + +
    + + +`; + +exports[`should render only children 1`] = ` + +
    + +`; + +exports[`should render when closeOnClick=true 1`] = ` + +
    + +
    + + +`; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap index 4a369aceaa4..4127f6e586e 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap @@ -8,22 +8,9 @@ exports[`should open & close 1`] = ` onMouseLeave={[Function]} /> -
    -
    - -
    -
    -
    + `; @@ -56,22 +43,9 @@ exports[`should render 2`] = ` onMouseLeave={[Function]} /> -
    -
    - -
    -
    -
    + `; diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js index 5f814a5347e..0598c4f9f07 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.js +++ b/server/sonar-web/src/main/js/components/issue/IssueView.js @@ -51,15 +51,14 @@ export default class IssueView extends React.PureComponent { handleCheck = (event /*: Event */) => { event.preventDefault(); - event.stopPropagation(); if (this.props.onCheck) { this.props.onCheck(this.props.issue.key, event); } }; handleClick = (event /*: Event & { target: HTMLElement } */) => { - event.preventDefault(); - if (this.props.onClick) { + if (!isClickable(event.target) && this.props.onClick) { + event.preventDefault(); this.props.onClick(this.props.issue.key); } }; @@ -100,12 +99,12 @@ export default class IssueView extends React.PureComponent { togglePopup={this.props.togglePopup} /> {issue.comments && issue.comments.length > 0 && ( @@ -114,8 +113,8 @@ export default class IssueView extends React.PureComponent { ))}
    @@ -137,3 +136,12 @@ export default class IssueView extends React.PureComponent { ); } } + +function isClickable(node /*: any */) { + if (!node) { + return false; + } + const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA']; + const tagName = (node.tagName || '').toUpperCase(); + return clickableTags.includes(tagName) || isClickable(node.parentNode); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js index 3f7d5e84ac7..783fdec65b0 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js @@ -19,9 +19,11 @@ */ // @flow import React from 'react'; -import Avatar from '../../../components/ui/Avatar'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetAssigneePopup from '../popups/SetAssigneePopup'; +import Avatar from '../../../components/ui/Avatar'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -43,6 +45,10 @@ export default class IssueAssign extends React.PureComponent { this.props.togglePopup('assign', open); }; + handleClose = () => { + this.toggleAssign(false); + }; + renderAssignee() { const { issue } = this.props; return ( @@ -67,24 +73,26 @@ export default class IssueAssign extends React.PureComponent { render() { if (this.props.canAssign) { return ( - - }> - - +
    + + }> + + +
    ); } else { return this.renderAssignee(); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js index 43f5adc2d91..a70f6ca5a33 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js @@ -19,11 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import ChangelogPopup from '../popups/ChangelogPopup'; import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Toggler from '../../../components/controls/Toggler'; import Tooltip from '../../../components/controls/Tooltip'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -39,35 +40,39 @@ type Props = { export default class IssueChangelog extends React.PureComponent { /*:: props: Props; */ - handleClick = (evt /*: SyntheticInputEvent */) => { - evt.preventDefault(); + toggleChangelog = (open /*: boolean | void */) => { + this.props.togglePopup('changelog', open); + }; + + handleClick = () => { this.toggleChangelog(); }; - toggleChangelog = (open /*: boolean | void */) => { - this.props.togglePopup('changelog', open); + handleClose = () => { + this.toggleChangelog(false); }; render() { return ( - }> - }> - - - +
    + }> + }> + + + +
    ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js index 01ba1f984a1..814d4053cf4 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js @@ -20,7 +20,8 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Toggler from '../../../components/controls/Toggler'; +import { Button } from '../../../components/ui/buttons'; import CommentPopup from '../popups/CommentPopup'; import { addIssueComment } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; @@ -49,29 +50,33 @@ export default class IssueCommentAction extends React.PureComponent { this.props.toggleComment(false); }; - handleCommentClick = () => this.props.toggleComment(); + handleCommentClick = () => { + this.props.toggleComment(); + }; + + handleClose = () => { + this.props.toggleComment(false); + }; render() { return ( -
  • - + }> - - + +
  • ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js index 1773bd16a0c..3a0910f6552 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import Avatar from '../../../components/ui/Avatar'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import Toggler from '../../../components/controls/Toggler'; import EditIcon from '../../../components/icons-components/EditIcon'; import { EditButton, DeleteButton } from '../../../components/ui/buttons'; import CommentDeletePopup from '../popups/CommentDeletePopup'; @@ -48,12 +48,6 @@ export default class IssueCommentLine extends React.PureComponent { openPopup: '' }; - handleCommentClick = (event /*: Event & {target: HTMLElement}*/) => { - if (event.target.tagName === 'A') { - event.stopPropagation(); - } - }; - handleEdit = (text /*: string */) => { this.props.onEdit(this.props.comment.key, text); this.toggleEditPopup(false); @@ -75,9 +69,17 @@ export default class IssueCommentLine extends React.PureComponent { }); }; - toggleDeletePopup = (force /*: ?boolean */) => this.togglePopup('delete', force); + toggleDeletePopup = (force /*: ?boolean */) => { + this.togglePopup('delete', force); + }; - toggleEditPopup = (force /*: ?boolean */) => this.togglePopup('edit', force); + toggleEditPopup = (force /*: ?boolean */) => { + this.togglePopup('edit', force); + }; + + closePopups = () => { + this.setState({ openPopup: '' }); + }; render() { const { comment } = this.props; @@ -95,49 +97,45 @@ export default class IssueCommentLine extends React.PureComponent {
    {comment.updatable && ( - + + }> + - }> - - + +
    )} {comment.updatable && ( - }> - - +
    + }> + + +
    )}
    diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js index 888d7f2d521..1f8a9cb2ae5 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js @@ -38,7 +38,6 @@ export default class IssueMessage extends React.PureComponent { handleClick = (e /*: MouseEvent */) => { e.preventDefault(); - e.stopPropagation(); this.context.workspace.openRule({ key: this.props.rule, organization: this.props.organization diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js index 326e64e64ab..f75e0e8f3c7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js @@ -19,10 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetSeverityPopup from '../popups/SetSeverityPopup'; -import SeverityHelper from '../../../components/shared/SeverityHelper'; import { setIssueSeverity } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import SeverityHelper from '../../../components/shared/SeverityHelper'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -42,28 +44,31 @@ export default class IssueSeverity extends React.PureComponent { this.props.togglePopup('set-severity', open); }; - setSeverity = (severity /*: string */) => + setSeverity = (severity /*: string */) => { this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity); + }; + + handleClose = () => { + this.toggleSetSeverity(false); + }; render() { const { issue } = this.props; if (this.props.canSetSeverity) { return ( - }> - - +
    + }> + + +
    ); } else { return ; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js index fb183a7f8b7..b9f1fcba0be 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js @@ -20,10 +20,11 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetIssueTagsPopup from '../popups/SetIssueTagsPopup'; -import TagsList from '../../../components/tags/TagsList'; import { setIssueTags } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import TagsList from '../../../components/tags/TagsList'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -57,32 +58,39 @@ export default class IssueTags extends React.PureComponent { ); }; + handleClose = () => { + this.toggleSetTags(false); + }; + render() { const { issue } = this.props; const { tags = [] } = issue; if (this.props.canSetTags) { return ( - - } - position="bottomright" - togglePopup={this.toggleSetTags}> - - +
    + + }> + + +
    ); } else { return ( diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js index 0dad342b1b5..781eebd1f06 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -45,8 +45,6 @@ type Props = {| |}; */ -const stopPropagation = (event /*: Event */) => event.stopPropagation(); - export default function IssueTitleBar(props /*: Props */) { const { issue } = props; const hasSimilarIssuesFilter = props.onFilter != null; @@ -103,7 +101,7 @@ export default function IssueTitleBar(props /*: Props */) { {displayLocations && (
  • {props.displayLocationsLink ? ( - + {locationsBadge} ) : ( @@ -112,11 +110,7 @@ export default function IssueTitleBar(props /*: Props */) {
  • )}
  • - +
  • diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js index 28379cde1cd..3aced6f8685 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js @@ -20,10 +20,12 @@ // @flow import React from 'react'; import { updateIssue } from '../actions'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetTransitionPopup from '../popups/SetTransitionPopup'; -import StatusHelper from '../../../components/shared/StatusHelper'; import { setIssueTransition } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import StatusHelper from '../../../components/shared/StatusHelper'; +import { Button } from '../../../components/ui/buttons'; /*:: import type { Issue } from '../types'; */ /*:: @@ -53,36 +55,41 @@ export default class IssueTransition extends React.PureComponent { this.props.togglePopup('transition', open); }; + handleClose = () => { + this.toggleSetTransition(false); + }; + render() { const { issue } = this.props; if (this.props.hasTransitions) { return ( - - }> - - +
    + + }> + + +
    ); } else { return ( ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.js b/server/sonar-web/src/main/js/components/issue/components/IssueType.js index 60941288e4b..380e8668a32 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueType.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.js @@ -19,10 +19,12 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; -import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SetTypePopup from '../popups/SetTypePopup'; import { setIssueType } from '../../../api/issues'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -43,26 +45,32 @@ export default class IssueType extends React.PureComponent { this.props.togglePopup('set-type', open); }; - setType = (type /*: string */) => + setType = (type /*: string */) => { this.props.setIssueProperty('type', 'set-type', setIssueType, type); + }; + + handleClose = () => { + this.toggleSetType(false); + }; render() { const { issue } = this.props; if (this.props.canSetSeverity) { return ( - }> - - +
    + }> + + +
    ); } else { return ( diff --git a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js index b9da955025a..5b900ac2e48 100644 --- a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js +++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js @@ -19,8 +19,10 @@ */ // @flow import React from 'react'; -import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SimilarIssuesPopup from '../popups/SimilarIssuesPopup'; +import Toggler from '../../../components/controls/Toggler'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; /*:: import type { Issue } from '../types'; */ @@ -51,20 +53,26 @@ export default class SimilarIssuesFilter extends React.PureComponent { this.props.togglePopup('similarIssues', open); }; + handleClose = () => { + this.togglePopup(false); + }; + render() { return ( - }> - - +
    + }> + + +
    ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js index cdd7cfeb6f1..ad39643695e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js @@ -34,8 +34,8 @@ it('should render without the action when the correct rights are missing', () => canAssign={false} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={jest.fn()} /> ); @@ -48,8 +48,8 @@ it('should render with the action', () => { canAssign={true} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={jest.fn()} /> ); @@ -63,12 +63,12 @@ it('should open the popup when the button is clicked', () => { canAssign={true} isOpen={false} issue={issue} - onFail={jest.fn()} onAssign={jest.fn()} + onFail={jest.fn()} togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js index 0b2b9071cd8..8e03fdef42a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -52,7 +52,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js index d54334a61f9..2114f366226 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js @@ -25,8 +25,8 @@ import { click } from '../../../../helpers/testUtils'; it('should render correctly', () => { const element = shallow( { const toggle = jest.fn(); const element = shallow( ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls.length).toBe(1); element.setProps({ currentPopup: 'comment' }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js index a321192729a..90cc0e1d8dc 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js @@ -53,9 +53,9 @@ it('should open the right popups when the buttons are clicked', () => { const element = shallow( ); - element.find('.js-issue-comment-edit').prop('onClick')(); + click(element.find('.js-issue-comment-edit')); expect(element.state()).toMatchSnapshot(); - element.find('.js-issue-comment-delete').prop('onClick')(); + click(element.find('.js-issue-comment-delete')); expect(element.state()).toMatchSnapshot(); element.update(); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js index 98e4cf45c5b..08b00404347 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js @@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js index aa353241a8a..81d9a512a45 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js @@ -71,7 +71,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js index 9bfc5d7cd85..98da66e0449 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js @@ -84,7 +84,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js index de6dcde42bf..51132f338ca 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js @@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => { togglePopup={toggle} /> ); - click(element.find('button')); + click(element.find('Button')); expect(toggle.mock.calls).toMatchSnapshot(); element.setProps({ isOpen: true }); expect(element).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap index d4ff90579c6..d768c0c852d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap @@ -4,112 +4,111 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "assign", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + + +
    `; exports[`should render with the action 1`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + + +
    `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap index 97f8c58d32a..0628de785bf 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap @@ -10,91 +10,97 @@ Array [ `; exports[`should open the popup when the button is clicked 2`] = ` - - } - position="bottomright" - togglePopup={[Function]} +
    - } > - - - + + + +
    `; exports[`should render correctly 1`] = ` - - } - position="bottomright" - togglePopup={[Function]} +
    - } > - - - + + + +
    `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap index 7305e5d8472..3c62307690e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap @@ -2,13 +2,13 @@ exports[`should open the popup when the button is clicked 1`] = `
  • - } - position="bottomleft" - togglePopup={ - [MockFunction] { - "calls": Array [ - Array [], - ], - } - } > - - + +
  • `; exports[`should render correctly 1`] = `
  • - } - position="bottomleft" - togglePopup={[MockFunction]} > - - + +
  • `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap index 71f4ef39f22..4db5ed5aa9c 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap @@ -35,9 +35,6 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` "__html": "test", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} />
    - test", - "key": "comment-key", - "updatable": true, +
    + test", + "key": "comment-key", + "updatable": true, + } } - } - customClass="issue-edit-comment-bubble-popup" - onComment={[Function]} - placeholder="" - toggleComment={[Function]} + onComment={[Function]} + placeholder="" + toggleComment={[Function]} + /> + } + > + - } - position="bottomright" - togglePopup={[Function]} + +
    +
    - - - } - } - popup={ - + - } - position="bottomright" - togglePopup={[Function]} - > - - + +
    `; @@ -133,9 +123,6 @@ exports[`should render correctly a comment that is not updatable 1`] = ` "__html": "test", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} />
    test", } } - onClick={[Function]} - role="Listitem" - tabIndex={0} />
    - test", - "key": "comment-key", - "updatable": true, +
    + test", + "key": "comment-key", + "updatable": true, + } } - } - customClass="issue-edit-comment-bubble-popup" - onComment={[Function]} - placeholder="" - toggleComment={[Function]} + onComment={[Function]} + placeholder="" + toggleComment={[Function]} + /> + } + > + - } - position="bottomright" - togglePopup={[Function]} + +
    +
    - - - } - } - popup={ - + - } - position="bottomright" - togglePopup={[Function]} - > - - + +
    `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap index 98eb8644b75..898a75fd3d6 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap @@ -4,80 +4,77 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "set-severity", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render with the action 1`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap index fa30aff7c20..ca33fb7f1ec 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap @@ -4,88 +4,85 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "edit-tags", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` - - } - position="bottomright" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render with the action 1`] = ` - - } - position="bottomright" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap index 30fc9e84c33..8f3694dd8d0 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -68,7 +68,6 @@ exports[`should render the titlebar correctly 1`] = ` > - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render with a resolution 1`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render with the action 1`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render without the action when there is no transitions 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap index 77988d1f003..1eb2844dbf6 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap @@ -4,82 +4,79 @@ exports[`should open the popup when the button is clicked 1`] = ` Array [ Array [ "set-type", - Object { - "currentTarget": Object { - "blur": [Function], - }, - "preventDefault": [Function], - "stopPropagation": [Function], - "target": Object { - "blur": [Function], - }, - }, + undefined, ], ] `; exports[`should open the popup when the button is clicked 2`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render with the action 1`] = ` - - } - position="bottomleft" - togglePopup={[Function]} +
    - - + + +
    `; exports[`should render without the action when the correct rights are missing 1`] = ` diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js index b3f230810fe..fcfd515b587 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js @@ -22,9 +22,9 @@ import React from 'react'; import { getIssueChangelog } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; import Avatar from '../../../components/ui/Avatar'; -import BubblePopup from '../../../components/common/BubblePopup'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../components/IssueChangelogDiff'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { ChangelogDiff } from '../components/IssueChangelogDiff'; */ /*:: import type { Issue } from '../types'; */ @@ -80,8 +80,8 @@ export default class ChangelogPopup extends React.PureComponent { const { issue } = this.props; const { author } = issue; return ( - -
    + +
    @@ -110,14 +110,14 @@ export default class ChangelogPopup extends React.PureComponent { {item.userName}

    )} - {item.diffs.map(diff => )} + {item.diffs.map(diff => )} ))}
    - +
    ); } } diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js index 9776ab00253..c7fd5d89ae8 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js @@ -19,8 +19,9 @@ */ // @flow import React from 'react'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; -import BubblePopup from '../../../components/common/BubblePopup'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: type Props = { @@ -31,13 +32,13 @@ type Props = { export default function CommentDeletePopup(props /*: Props */) { return ( - -
    + +
    {translate('issue.comment.delete_confirm_message')}
    - +
    - +
    ); } diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js index 2a17f71383b..bee719a083e 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js @@ -19,16 +19,15 @@ */ // @flow import React from 'react'; -import classNames from 'classnames'; -import BubblePopup from '../../../components/common/BubblePopup'; import MarkdownTips from '../../../components/common/MarkdownTips'; +import { Button, ResetButtonLink } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; +import { DropdownOverlay } from '../../controls/Dropdown'; /*:: import type { IssueComment } from '../types'; */ /*:: type Props = { comment?: IssueComment, - customClass?: string, onComment: string => void, toggleComment: boolean => void, placeholder: string, @@ -63,8 +62,7 @@ export default class CommentPopup extends React.PureComponent { } }; - handleCancelClick = (evt /*: MouseEvent */) => { - evt.preventDefault(); + handleCancelClick = () => { this.props.toggleComment(false); }; @@ -81,37 +79,37 @@ export default class CommentPopup extends React.PureComponent { render() { const { comment } = this.props; return ( - -
    -