aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx13
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx45
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx54
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx28
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx52
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap299
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap751
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx25
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx47
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx85
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap66
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap373
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx205
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap384
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.js93
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResults.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js10
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/bubble-popup.css186
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/dropdowns.css154
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/issues.css10
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/menu.css24
-rw-r--r--server/sonar-web/src/main/js/app/styles/sonar.css1
-rw-r--r--server/sonar-web/src/main/js/app/styles/style.css2
-rw-r--r--server/sonar-web/src/main/js/app/theme.js3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx158
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx)13
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx (renamed from server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx)43
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap48
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap89
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap272
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx83
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx150
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx)13
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx (renamed from server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx)27
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap49
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap150
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx106
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx (renamed from server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap30
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx40
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js72
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap120
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js42
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js57
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js33
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js29
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap178
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap103
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css57
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx116
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap137
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx70
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap134
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css6
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx80
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx (renamed from server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js)13
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js16
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js104
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap27
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js46
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js64
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js47
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap40
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx100
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap254
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/PageActions.tsx94
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap157
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserActions.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx90
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx96
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx15
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx21
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx20
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx26
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap67
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/styles.css7
-rw-r--r--server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js25
-rw-r--r--server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx111
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js143
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap20
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap182
-rw-r--r--server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx34
-rw-r--r--server/sonar-web/src/main/js/components/controls/DateInput.tsx132
-rw-r--r--server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx (renamed from server/sonar-web/src/main/js/components/common/BubblePopup.tsx)48
-rw-r--r--server/sonar-web/src/main/js/components/controls/Dropdown.tsx147
-rw-r--r--server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx108
-rw-r--r--server/sonar-web/src/main/js/components/controls/Toggler.tsx102
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tooltip.tsx79
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx100
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx98
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx71
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap512
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap50
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap38
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.js20
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueAssign.js48
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js31
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js84
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessage.js1
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js41
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.js52
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTransition.js49
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueType.js40
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js34
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js8
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js6
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js4
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap183
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap142
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap42
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap203
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap121
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap137
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap190
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap125
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js13
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js64
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js13
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js13
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js13
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js18
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap12
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap138
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js16
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap9
-rw-r--r--server/sonar-web/src/main/js/components/tags/TagsSelector.tsx26
-rw-r--r--server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap86
-rw-r--r--server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/ui/popups.css213
-rw-r--r--server/sonar-web/src/main/js/components/ui/popups.tsx64
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts14
225 files changed, 6788 insertions, 6768 deletions
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
index 637a95e3fca..5280e553a75 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -22,14 +22,13 @@ import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
import { SuggestionLink } from './SuggestionsProvider';
import { CurrentUser, isLoggedIn } from '../../types';
-import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
+import { DropdownOverlay } from '../../../components/controls/Dropdown';
interface Props {
currentUser: CurrentUser;
onClose: () => void;
- popupPosition?: BubblePopupPosition;
suggestions: Array<SuggestionLink>;
}
@@ -46,7 +45,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
};
renderTitle(text: string) {
- return <li className="dropdown-header">{text}</li>;
+ return <li className="menu-header">{text}</li>;
}
renderSuggestions() {
@@ -147,10 +146,8 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
render() {
return (
- <BubblePopup
- customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup"
- position={this.props.popupPosition}>
- <ul className="menu">
+ <DropdownOverlay>
+ <ul className="menu abs-width-240">
{this.renderSuggestions()}
<li>
<Link onClick={this.props.onClose} to="/documentation">
@@ -165,7 +162,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
{this.context.onSonarCloud && this.renderSonarCloudLinks()}
{!this.context.onSonarCloud && this.renderSonarQubeLinks()}
</ul>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
index 29b58e4e6e3..20ddca01c15 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
@@ -21,9 +21,9 @@ import * as React from 'react';
import EmbedDocsPopup from './EmbedDocsPopup';
import { SuggestionLink } from './SuggestionsProvider';
import { CurrentUser } from '../../types';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
-import HelpIcon from '../../../components/icons-components/HelpIcon';
+import Toggler from '../../../components/controls/Toggler';
import Tooltip from '../../../components/controls/Tooltip';
+import HelpIcon from '../../../components/icons-components/HelpIcon';
import { translate } from '../../../helpers/l10n';
interface Props {
@@ -63,6 +63,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
+ event.currentTarget.blur();
this.toggleHelp();
};
@@ -78,26 +79,26 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta
render() {
return (
- <BubblePopupHelper
- isOpen={this.state.helpOpen}
- offset={{ horizontal: 12, vertical: -10 }}
- popup={
- <EmbedDocsPopup
- currentUser={this.props.currentUser}
- onClose={this.closeHelp}
- suggestions={this.props.suggestions}
- />
- }
- position="bottomleft"
- togglePopup={this.setHelpDisplay}>
- <Tooltip
- overlay={this.props.tooltip ? translate('tutorials.follow_later') : undefined}
- visible={this.props.showTooltip}>
- <a className="navbar-help" href="#" onClick={this.handleClick}>
- <HelpIcon />
- </a>
- </Tooltip>
- </BubblePopupHelper>
+ <li className="dropdown">
+ <Toggler
+ onRequestClose={this.closeHelp}
+ open={this.state.helpOpen}
+ overlay={
+ <EmbedDocsPopup
+ currentUser={this.props.currentUser}
+ onClose={this.closeHelp}
+ suggestions={this.props.suggestions}
+ />
+ }>
+ <Tooltip
+ overlay={this.props.tooltip ? translate('tutorials.follow_later') : undefined}
+ visible={this.props.showTooltip}>
+ <a className="navbar-help" href="#" onClick={this.handleClick}>
+ <HelpIcon />
+ </a>
+ </Tooltip>
+ </Toggler>
+ </li>
);
}
}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
index b1af003796f..0984724bfcd 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
@@ -1,15 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should display suggestion links 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup"
->
+<DropdownOverlay>
<ul
- className="menu"
+ className="menu abs-width-240"
>
<React.Fragment>
<li
- className="dropdown-header"
+ className="menu-header"
>
embed_docs.suggestion
</li>
@@ -74,7 +72,7 @@ exports[`should display suggestion links 1`] = `
className="divider"
/>
<li
- className="dropdown-header"
+ className="menu-header"
>
embed_docs.get_support
</li>
@@ -114,7 +112,7 @@ exports[`should display suggestion links 1`] = `
className="divider"
/>
<li
- className="dropdown-header"
+ className="menu-header"
>
embed_docs.stay_connected
</li>
@@ -152,5 +150,5 @@ exports[`should display suggestion links 1`] = `
</li>
</React.Fragment>
</ul>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
index 890e4d5f29e..a918d734e24 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import * as PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
@@ -35,6 +34,7 @@ import {
import { translate } from '../../../../helpers/l10n';
import PlusCircleIcon from '../../../../components/icons-components/PlusCircleIcon';
import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import Toggler from '../../../../components/controls/Toggler';
import Tooltip from '../../../../components/controls/Tooltip';
interface Props {
@@ -91,19 +91,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
}
};
- renderDropdown = () => {
- const { configuration } = this.props.component;
- return this.state.dropdownOpen ? (
- <ComponentNavBranchesMenu
- branchLikes={this.props.branchLikes}
- canAdmin={configuration && configuration.showSettings}
- component={this.props.component}
- currentBranchLike={this.props.currentBranchLike}
- onClose={this.closeDropdown}
- />
- ) : null;
- };
-
renderMergeBranch = () => {
const { currentBranchLike } = this.props;
if (isShortLivingBranch(currentBranchLike)) {
@@ -140,6 +127,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
render() {
const { branchLikes, currentBranchLike } = this.props;
+ const { configuration } = this.props.component;
if (this.context.onSonarCloud && !this.context.branchesEnabled) {
return null;
@@ -176,18 +164,32 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
}
return (
- <div
- className={classNames('navbar-context-branches', 'dropdown', {
- open: this.state.dropdownOpen
- })}>
- <a className="link-base-color link-no-underline nowrap" href="#" onClick={this.handleClick}>
- <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
- <Tooltip mouseEnterDelay={1} overlay={displayName}>
- <span className="text-limited text-top">{displayName}</span>
- </Tooltip>
- <i className="icon-dropdown little-spacer-left" />
- </a>
- {this.renderDropdown()}
+ <div className="navbar-context-branches">
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.closeDropdown}
+ open={this.state.dropdownOpen}
+ overlay={
+ <ComponentNavBranchesMenu
+ branchLikes={this.props.branchLikes}
+ canAdmin={configuration && configuration.showSettings}
+ component={this.props.component}
+ currentBranchLike={this.props.currentBranchLike}
+ onClose={this.closeDropdown}
+ />
+ }>
+ <a
+ className="link-base-color link-no-underline nowrap"
+ href="#"
+ onClick={this.handleClick}>
+ <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
+ <Tooltip mouseEnterDelay={1} overlay={displayName}>
+ <span className="text-limited text-top">{displayName}</span>
+ </Tooltip>
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
+ </Toggler>
+ </div>
{this.renderMergeBranch()}
</div>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
index 5772988c123..98f8de579cf 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
@@ -36,6 +36,7 @@ import { translate } from '../../../../helpers/l10n';
import { getBranchLikeUrl } from '../../../../helpers/urls';
import SearchBox from '../../../../components/controls/SearchBox';
import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import { DropdownOverlay } from '../../../../components/controls/Dropdown';
interface Props {
branchLikes: BranchLike[];
@@ -51,7 +52,6 @@ interface State {
}
export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
- private node?: HTMLElement | null;
private listNode?: HTMLUListElement | null;
private selectedBranchNode?: HTMLLIElement | null;
@@ -59,13 +59,9 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
router: PropTypes.object
};
- constructor(props: Props) {
- super(props);
- this.state = { query: '', selected: undefined };
- }
+ state: State = { query: '', selected: undefined };
componentDidMount() {
- window.addEventListener('click', this.handleClickOutside);
this.scrollToSelectedBranch(false);
}
@@ -73,10 +69,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
this.scrollToSelectedBranch(true);
}
- componentWillUnmount() {
- window.removeEventListener('click', this.handleClickOutside);
- }
-
scrollToSelectedBranch(smooth: boolean) {
if (this.listNode && this.selectedBranchNode) {
scrollToElement(this.selectedBranchNode, {
@@ -97,12 +89,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
});
};
- handleClickOutside = (event: Event) => {
- if (!this.node || !this.node.contains(event.target as HTMLElement)) {
- this.props.onClose();
- }
- };
-
handleSearchChange = (query: string) => this.setState({ query, selected: undefined });
handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
@@ -212,7 +198,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
<React.Fragment key={getBranchLikeKey(branchLike)}>
{showDivider && <li className="divider" />}
{showOrphanHeader && (
- <li className="dropdown-header">
+ <li className="menu-header">
<div className="display-inline-block text-middle">
{translate('branches.orphan_branches')}
</div>
@@ -223,12 +209,12 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
</li>
)}
{showPullRequestHeader && (
- <li className="dropdown-header navbar-context-meta-branch-menu-title">
+ <li className="menu-header navbar-context-meta-branch-menu-title">
{translate('branches.pull_requests')}
</li>
)}
{showShortLivingBranchHeader && (
- <li className="dropdown-header navbar-context-meta-branch-menu-title">
+ <li className="menu-header navbar-context-meta-branch-menu-title">
{translate('branches.short_lived_branches')}
</li>
)}
@@ -261,7 +247,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
component.configuration.showSettings;
return (
- <div className="dropdown-menu" ref={node => (this.node = node)}>
+ <DropdownOverlay noPadding={true}>
{this.renderSearch()}
{this.renderBranchesList()}
{showManageLink && (
@@ -273,7 +259,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
</Link>
</div>
)}
- </div>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index 3a78c92dc46..86ea6e3c84a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -183,19 +183,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}
return (
- <Dropdown data-test="extensions">
+ <Dropdown
+ data-test="administration"
+ overlay={<ul className="menu">{adminLinks}</ul>}
+ tagName="li">
{({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: isSettingsActive || open })}
- href="#"
- id="component-navigation-admin"
- onClick={onToggleClick}>
- {translate('layout.settings')}
- <i className="icon-dropdown little-spacer-left" />
- </a>
- <ul className="dropdown-menu">{adminLinks}</ul>
- </li>
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', { active: isSettingsActive || open })}
+ href="#"
+ id="component-navigation-admin"
+ onClick={onToggleClick}>
+ {translate('layout.settings')}
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
)}
</Dropdown>
);
@@ -421,19 +423,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}
return (
- <Dropdown data-test="admin-extensions">
+ <Dropdown
+ data-test="extensions"
+ overlay={<ul className="menu">{extensions.map(e => this.renderExtension(e, false))}</ul>}
+ tagName="li">
{({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: open })}
- href="#"
- id="component-navigation-more"
- onClick={onToggleClick}>
- {translate('more')}
- <i className="icon-dropdown little-spacer-left" />
- </a>
- <ul className="dropdown-menu">{extensions.map(e => this.renderExtension(e, false))}</ul>
- </li>
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', { active: open })}
+ href="#"
+ id="component-navigation-more"
+ onClick={onToggleClick}>
+ {translate('more')}
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
)}
</Dropdown>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
index 8ea67da8b97..83a894a3b38 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
@@ -99,9 +99,9 @@ it('opens menu', () => {
/>,
{ context: { branchesEnabled: true } }
);
- expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0);
+ expect(wrapper.find('Toggler').prop('open')).toBe(false);
click(wrapper.find('a'));
- expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1);
+ expect(wrapper.find('Toggler').prop('open')).toBe(true);
});
it('renders single branch popup', () => {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
index 4ac93c4fb00..a7b7bb95953 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
@@ -41,8 +41,8 @@ it('should work with extensions', () => {
const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
context: { branchesEnabled: true }
});
- expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot();
- expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
+ expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
it('should work with multiple extensions', () => {
@@ -60,8 +60,8 @@ it('should work with multiple extensions', () => {
const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
context: { branchesEnabled: true }
});
- expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot();
- expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
+ expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
it('should work for short-living branches', () => {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
index dc23c7b4368..3d9fee6a289 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
@@ -2,36 +2,71 @@
exports[`renders main branch 1`] = `
<div
- className="navbar-context-branches dropdown"
+ className="navbar-context-branches"
>
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
+ <div
+ className="dropdown"
>
- <BranchIcon
- branchLike={
- Object {
- "isMain": true,
- "name": "master",
- }
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <ComponentNavBranchesMenu
+ branchLikes={
+ Array [
+ Object {
+ "isMain": true,
+ "name": "master",
+ },
+ Object {
+ "isMain": false,
+ "name": "foo",
+ "type": "LONG",
+ },
+ ]
+ }
+ canAdmin={undefined}
+ component={Object {}}
+ currentBranchLike={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ onClose={[Function]}
+ />
}
- className="little-spacer-right"
- />
- <Tooltip
- mouseEnterDelay={1}
- overlay="master"
>
- <span
- className="text-limited text-top"
+ <a
+ className="link-base-color link-no-underline nowrap"
+ href="#"
+ onClick={[Function]}
>
- master
- </span>
- </Tooltip>
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
+ <BranchIcon
+ branchLike={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ className="little-spacer-right"
+ />
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="master"
+ >
+ <span
+ className="text-limited text-top"
+ >
+ master
+ </span>
+ </Tooltip>
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </a>
+ </Toggler>
+ </div>
</div>
`;
@@ -49,39 +84,80 @@ exports[`renders no branch support popup 1`] = `
exports[`renders pull request 1`] = `
<div
- className="navbar-context-branches dropdown"
+ className="navbar-context-branches"
>
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
+ <div
+ className="dropdown"
>
- <BranchIcon
- branchLike={
- Object {
- "base": "master",
- "branch": "feature",
- "key": "1234",
- "title": "Feature PR",
- "url": "https://example.com/pull/1234",
- }
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <ComponentNavBranchesMenu
+ branchLikes={
+ Array [
+ Object {
+ "base": "master",
+ "branch": "feature",
+ "key": "1234",
+ "title": "Feature PR",
+ "url": "https://example.com/pull/1234",
+ },
+ Object {
+ "isMain": false,
+ "name": "foo",
+ "type": "LONG",
+ },
+ ]
+ }
+ canAdmin={undefined}
+ component={Object {}}
+ currentBranchLike={
+ Object {
+ "base": "master",
+ "branch": "feature",
+ "key": "1234",
+ "title": "Feature PR",
+ "url": "https://example.com/pull/1234",
+ }
+ }
+ onClose={[Function]}
+ />
}
- className="little-spacer-right"
- />
- <Tooltip
- mouseEnterDelay={1}
- overlay="1234 – Feature PR"
>
- <span
- className="text-limited text-top"
+ <a
+ className="link-base-color link-no-underline nowrap"
+ href="#"
+ onClick={[Function]}
>
- 1234 – Feature PR
- </span>
- </Tooltip>
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
+ <BranchIcon
+ branchLike={
+ Object {
+ "base": "master",
+ "branch": "feature",
+ "key": "1234",
+ "title": "Feature PR",
+ "url": "https://example.com/pull/1234",
+ }
+ }
+ className="little-spacer-right"
+ />
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="1234 – Feature PR"
+ >
+ <span
+ className="text-limited text-top"
+ >
+ 1234 – Feature PR
+ </span>
+ </Tooltip>
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </a>
+ </Toggler>
+ </div>
<span
className="note big-spacer-left text-ellipsis flex-shrink"
>
@@ -105,44 +181,95 @@ exports[`renders pull request 1`] = `
exports[`renders short-living branch 1`] = `
<div
- className="navbar-context-branches dropdown"
+ className="navbar-context-branches"
>
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
+ <div
+ className="dropdown"
>
- <BranchIcon
- branchLike={
- Object {
- "isMain": false,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "bugs": 0,
- "codeSmells": 0,
- "qualityGateStatus": "OK",
- "vulnerabilities": 0,
- },
- "type": "SHORT",
- }
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <ComponentNavBranchesMenu
+ branchLikes={
+ Array [
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "foo",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 0,
+ "qualityGateStatus": "OK",
+ "vulnerabilities": 0,
+ },
+ "type": "SHORT",
+ },
+ Object {
+ "isMain": false,
+ "name": "foo",
+ "type": "LONG",
+ },
+ ]
+ }
+ canAdmin={undefined}
+ component={Object {}}
+ currentBranchLike={
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "foo",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 0,
+ "qualityGateStatus": "OK",
+ "vulnerabilities": 0,
+ },
+ "type": "SHORT",
+ }
+ }
+ onClose={[Function]}
+ />
}
- className="little-spacer-right"
- />
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
>
- <span
- className="text-limited text-top"
+ <a
+ className="link-base-color link-no-underline nowrap"
+ href="#"
+ onClick={[Function]}
>
- foo
- </span>
- </Tooltip>
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
+ <BranchIcon
+ branchLike={
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "foo",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 0,
+ "qualityGateStatus": "OK",
+ "vulnerabilities": 0,
+ },
+ "type": "SHORT",
+ }
+ }
+ className="little-spacer-right"
+ />
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ >
+ <span
+ className="text-limited text-top"
+ >
+ foo
+ </span>
+ </Tooltip>
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </a>
+ </Toggler>
+ </div>
<span
className="note big-spacer-left"
>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
index cac918b4985..3296a5ced18 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders list 1`] = `
-<div
- className="dropdown-menu"
+<DropdownOverlay
+ noPadding={true}
>
<div
className="menu-search"
@@ -43,7 +43,7 @@ exports[`renders list 1`] = `
key="pull-request-1234"
>
<li
- className="dropdown-header navbar-context-meta-branch-menu-title"
+ className="menu-header navbar-context-meta-branch-menu-title"
>
branches.pull_requests
</li>
@@ -80,7 +80,7 @@ exports[`renders list 1`] = `
className="divider"
/>
<li
- className="dropdown-header"
+ className="menu-header"
>
<div
className="display-inline-block text-middle"
@@ -181,7 +181,7 @@ exports[`renders list 1`] = `
className="divider"
/>
<li
- className="dropdown-header"
+ className="menu-header"
>
<div
className="display-inline-block text-middle"
@@ -221,12 +221,12 @@ exports[`renders list 1`] = `
/>
</React.Fragment>
</ul>
-</div>
+</DropdownOverlay>
`;
exports[`searches 1`] = `
-<div
- className="dropdown-menu"
+<DropdownOverlay
+ noPadding={true}
>
<div
className="menu-search"
@@ -246,7 +246,7 @@ exports[`searches 1`] = `
key="branch-foobar"
>
<li
- className="dropdown-header navbar-context-meta-branch-menu-title"
+ className="menu-header navbar-context-meta-branch-menu-title"
>
branches.short_lived_branches
</li>
@@ -303,5 +303,5 @@ exports[`searches 1`] = `
/>
</React.Fragment>
</ul>
-</div>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
index 249f20e49dc..3abe30c25f4 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
@@ -90,7 +90,82 @@ exports[`should work for all qualifiers 1`] = `
</Link>
</li>
<Dropdown
- data-test="extensions"
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/settings",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_settings.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/webhooks",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ webhooks.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/deletion",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ deletion.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
/>
</NavBarTabs>
`;
@@ -185,7 +260,31 @@ exports[`should work for all qualifiers 2`] = `
</Link>
</li>
<Dropdown
- data-test="extensions"
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/settings",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_settings.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
/>
</NavBarTabs>
`;
@@ -280,7 +379,31 @@ exports[`should work for all qualifiers 3`] = `
</Link>
</li>
<Dropdown
- data-test="extensions"
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/deletion",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ deletion.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
/>
</NavBarTabs>
`;
@@ -467,7 +590,31 @@ exports[`should work for all qualifiers 5`] = `
</Link>
</li>
<Dropdown
- data-test="extensions"
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/deletion",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ deletion.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
/>
</NavBarTabs>
`;
@@ -710,355 +857,291 @@ exports[`should work for short-living branches 1`] = `
`;
exports[`should work with extensions 1`] = `
-<li
- className="dropdown"
->
- <a
- className="dropdown-toggle"
- href="#"
- id="component-navigation-admin"
- onClick={[Function]}
- >
- layout.settings
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li
- key="settings"
+<Dropdown
+ data-test="extensions"
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/settings",
- "query": Object {
- "id": "foo",
- },
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/extension/component-foo",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- project_settings.page
- </Link>
- </li>
- <li
- key="branches"
+ >
+ ComponentFoo
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
+`;
+
+exports[`should work with extensions 2`] = `
+<Dropdown
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/branches",
- "query": Object {
- "id": "foo",
- },
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/settings",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- project_branches.page
- </Link>
- </li>
- <li
- key="webhooks"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/webhooks",
- "query": Object {
- "id": "foo",
- },
+ >
+ project_settings.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- webhooks.page
- </Link>
- </li>
- <li
- key="foo"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/admin/extension/foo",
- "query": Object {
- "id": "foo",
- },
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/webhooks",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- Foo
- </Link>
- </li>
- <li
- key="project_delete"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/deletion",
- "query": Object {
- "id": "foo",
- },
+ >
+ webhooks.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/admin/extension/foo",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- deletion.page
- </Link>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should work with extensions 2`] = `
-<li
- className="dropdown"
->
- <a
- className="dropdown-toggle"
- href="#"
- id="component-navigation-more"
- onClick={[Function]}
- >
- more
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li
- key="component-foo"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/extension/component-foo",
- "query": Object {
- "id": "foo",
- },
+ >
+ Foo
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/deletion",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- ComponentFoo
- </Link>
- </li>
- </ul>
-</li>
+ >
+ deletion.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
`;
exports[`should work with multiple extensions 1`] = `
-<li
- className="dropdown"
->
- <a
- className="dropdown-toggle"
- href="#"
- id="component-navigation-admin"
- onClick={[Function]}
- >
- layout.settings
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li
- key="settings"
+<Dropdown
+ data-test="extensions"
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/settings",
- "query": Object {
- "id": "foo",
- },
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/extension/component-foo",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- project_settings.page
- </Link>
- </li>
- <li
- key="branches"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/branches",
- "query": Object {
- "id": "foo",
- },
+ >
+ ComponentFoo
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/extension/component-bar",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- project_branches.page
- </Link>
- </li>
- <li
- key="webhooks"
+ >
+ ComponentBar
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
+`;
+
+exports[`should work with multiple extensions 2`] = `
+<Dropdown
+ data-test="administration"
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/webhooks",
- "query": Object {
- "id": "foo",
- },
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/settings",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- webhooks.page
- </Link>
- </li>
- <li
- key="foo"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/admin/extension/foo",
- "query": Object {
- "id": "foo",
- },
+ >
+ project_settings.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- Foo
- </Link>
- </li>
- <li
- key="bar"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/admin/extension/bar",
- "query": Object {
- "id": "foo",
- },
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/webhooks",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- Bar
- </Link>
- </li>
- <li
- key="project_delete"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/deletion",
- "query": Object {
- "id": "foo",
- },
+ >
+ webhooks.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/admin/extension/foo",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- deletion.page
- </Link>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should work with multiple extensions 2`] = `
-<li
- className="dropdown"
->
- <a
- className="dropdown-toggle"
- href="#"
- id="component-navigation-more"
- onClick={[Function]}
- >
- more
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li
- key="component-foo"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/extension/component-foo",
- "query": Object {
- "id": "foo",
- },
+ >
+ Foo
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/admin/extension/bar",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- ComponentFoo
- </Link>
- </li>
- <li
- key="component-bar"
- >
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/project/extension/component-bar",
- "query": Object {
- "id": "foo",
- },
+ >
+ Bar
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/deletion",
+ "query": Object {
+ "id": "foo",
+ },
+ }
}
- }
- >
- ComponentBar
- </Link>
- </li>
- </ul>
-</li>
+ >
+ deletion.page
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
index 866edfae805..9e1eabf9be6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
@@ -99,14 +99,12 @@ class GlobalNav extends React.PureComponent<Props, State> {
<ul className="global-navbar-menu pull-right">
<GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} />
- <li>
- <EmbedDocsPopupHelper
- currentUser={this.props.currentUser}
- showTooltip={this.state.onboardingTutorialTooltip}
- suggestions={this.props.suggestions}
- tooltip={!this.props.onSonarCloud}
- />
- </li>
+ <EmbedDocsPopupHelper
+ currentUser={this.props.currentUser}
+ showTooltip={this.state.onboardingTutorialTooltip}
+ suggestions={this.props.suggestions}
+ tooltip={!this.props.onSonarCloud}
+ />
<Search appState={this.props.appState} currentUser={this.props.currentUser} />
{isLoggedIn(this.props.currentUser) &&
this.props.onSonarCloud && (
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
index 443f074225c..cab30c446ae 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
@@ -153,19 +153,20 @@ export default class GlobalNavMenu extends React.PureComponent<Props> {
return null;
}
return (
- <Dropdown>
+ <Dropdown
+ overlay={<ul className="menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>}
+ tagName="li">
{({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: open })}
- href="#"
- id="global-navigation-more"
- onClick={onToggleClick}>
- {translate('more')}
- <span className="icon-dropdown little-spacer-left" />
- </a>
- <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>
- </li>
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', { active: open })}
+ href="#"
+ id="global-navigation-more"
+ onClick={onToggleClick}>
+ {translate('more')}
+ <span className="icon-dropdown little-spacer-left" />
+ </a>
)}
</Dropdown>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
index 1ae29cfc8ab..d58fb2379ac 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import * as PropTypes from 'prop-types';
import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm';
import PlusIcon from '../../../../components/icons-components/PlusIcon';
@@ -65,35 +64,41 @@ export default class GlobalNavPlus extends React.PureComponent<Props, State> {
render() {
return (
- <Dropdown>
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ <li>
+ <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
+ {translate('my_account.analyze_new_project')}
+ </a>
+ </li>
+ <li className="divider" />
+ <li>
+ <a className="js-new-organization" href="#" onClick={this.handleNewOrganizationClick}>
+ {translate('my_account.create_new_organization')}
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li">
{({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a className="navbar-plus" href="#" onClick={onToggleClick}>
+ <>
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className="navbar-plus"
+ href="#"
+ onClick={onToggleClick}>
<PlusIcon />
</a>
- <ul className="dropdown-menu dropdown-menu-right">
- <li>
- <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
- {translate('my_account.analyze_new_project')}
- </a>
- </li>
- <li className="divider" />
- <li>
- <a
- className="js-new-organization"
- href="#"
- onClick={this.handleNewOrganizationClick}>
- {translate('my_account.create_new_organization')}
- </a>
- </li>
- </ul>
+
{this.state.createOrganization && (
<CreateOrganizationForm
onClose={this.closeCreateOrganizationForm}
onCreate={this.handleCreateOrganization}
/>
)}
- </li>
+ </>
)}
</Dropdown>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
index b568f713458..af85d305412 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import { sortBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
@@ -63,52 +62,52 @@ export default class GlobalNavUser extends React.PureComponent<Props> {
const currentUser = this.props.currentUser as LoggedInUser;
const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', 'js-user-authenticated', { open })}>
- <a className="dropdown-toggle navbar-avatar" href="#" onClick={onToggleClick}>
- <Avatar
- hash={currentUser.avatar}
- name={currentUser.name}
- size={theme.globalNavContentHeightRaw}
- />
- </a>
- <ul className="dropdown-menu dropdown-menu-right">
- <li className="dropdown-item">
- <div className="text-ellipsis text-muted" title={currentUser.name}>
- <strong>{currentUser.name}</strong>
+ <Dropdown
+ className="js-user-authenticated"
+ overlay={
+ <ul className="menu">
+ <li className="menu-item">
+ <div className="text-ellipsis text-muted" title={currentUser.name}>
+ <strong>{currentUser.name}</strong>
+ </div>
+ {currentUser.email != null && (
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title={currentUser.email}>
+ {currentUser.email}
</div>
- {currentUser.email != null && (
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title={currentUser.email}>
- {currentUser.email}
- </div>
- )}
- </li>
- <li className="divider" />
- <li>
- <Link to="/account">{translate('my_account.page')}</Link>
- </li>
- {hasOrganizations && <li role="separator" className="divider" />}
- {hasOrganizations && (
- <li>
- <Link to="/account/organizations">{translate('my_organizations')}</Link>
- </li>
)}
- {hasOrganizations &&
- sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
- <OrganizationListItem key={organization.key} organization={organization} />
- ))}
- {hasOrganizations && <li role="separator" className="divider" />}
+ </li>
+ <li className="divider" />
+ <li>
+ <Link to="/account">{translate('my_account.page')}</Link>
+ </li>
+ {hasOrganizations && <li className="divider" role="separator" />}
+ {hasOrganizations && (
<li>
- <a onClick={this.handleLogout} href="#">
- {translate('layout.logout')}
- </a>
+ <Link to="/account/organizations">{translate('my_organizations')}</Link>
</li>
- </ul>
- </li>
- )}
+ )}
+ {hasOrganizations &&
+ sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+ <OrganizationListItem key={organization.key} organization={organization} />
+ ))}
+ {hasOrganizations && <li className="divider" role="separator" />}
+ <li>
+ <a href="#" onClick={this.handleLogout}>
+ {translate('layout.logout')}
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li">
+ <a className="dropdown-toggle navbar-avatar" href="#">
+ <Avatar
+ hash={currentUser.avatar}
+ name={currentUser.name}
+ size={theme.globalNavContentHeightRaw}
+ />
+ </a>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
index 22fa861f34f..ba791499f59 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
@@ -32,7 +32,7 @@ it('should work with extensions', () => {
const wrapper = shallow(
<GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
);
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('should show administration menu if the user has the rights', () => {
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
index 1e02139f691..3e4110f5193 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
@@ -25,14 +25,16 @@ import { click } from '../../../../../helpers/testUtils';
it('render', () => {
const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={jest.fn()} />);
expect(wrapper.is('Dropdown')).toBe(true);
- expect(wrapper.find('Dropdown').shallow()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('opens onboarding', () => {
const openOnboardingTutorial = jest.fn();
- const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />)
- .find('Dropdown')
- .shallow();
+ const wrapper = shallow(
+ shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />)
+ .find('Dropdown')
+ .prop('overlay')
+ );
click(wrapper.find('.js-new-project'));
expect(openOnboardingTutorial).toBeCalled();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
index ec18ecba3a9..152efb5d69c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
@@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => {
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} />
);
wrapper.setState({ open: true });
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('should render user organizations', () => {
@@ -51,7 +51,7 @@ it('should render user organizations', () => {
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} />
);
wrapper.setState({ open: true });
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('should not render user organizations when they are not activated', () => {
@@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => {
/>
);
wrapper.setState({ open: true });
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
index 27714386573..6eb526f021f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
@@ -77,34 +77,22 @@ exports[`should show administration menu if the user has the rights 1`] = `
`;
exports[`should work with extensions 1`] = `
-<li
- className="dropdown"
->
- <a
- className="dropdown-toggle"
- href="#"
- id="global-navigation-more"
- onClick={[Function]}
- >
- more
- <span
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li
- key="foo"
+<Dropdown
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/extension/foo"
- >
- Foo
- </Link>
- </li>
- </ul>
-</li>
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/extension/foo"
+ >
+ Foo
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
index 4400a02b250..1664c15d58c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
@@ -1,40 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
-<li
- className="dropdown"
->
- <a
- className="navbar-plus"
- href="#"
- onClick={[Function]}
- >
- <PlusIcon />
- </a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li>
- <a
- className="js-new-project"
- href="#"
- onClick={[Function]}
- >
- my_account.analyze_new_project
- </a>
- </li>
- <li
- className="divider"
- />
- <li>
- <a
- className="js-new-organization"
- href="#"
- onClick={[Function]}
- >
- my_account.create_new_organization
- </a>
- </li>
- </ul>
-</li>
+<Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <a
+ className="js-new-project"
+ href="#"
+ onClick={[Function]}
+ >
+ my_account.analyze_new_project
+ </a>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <a
+ className="js-new-organization"
+ href="#"
+ onClick={[Function]}
+ >
+ my_account.create_new_organization
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li"
+/>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
index e83123dfbdc..316ff4a8e44 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
@@ -1,13 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not render user organizations when they are not activated 1`] = `
-<li
- className="dropdown js-user-authenticated"
+<Dropdown
+ className="js-user-authenticated"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li
+ className="menu-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li"
>
<a
className="dropdown-toggle navbar-avatar"
href="#"
- onClick={[Function]}
>
<Connect(Avatar)
hash="abcd1234"
@@ -15,49 +59,7 @@ exports[`should not render user organizations when they are not activated 1`] =
size={32}
/>
</a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
+</Dropdown>
`;
exports[`should render the right interface for anonymous user 1`] = `
@@ -73,13 +75,57 @@ exports[`should render the right interface for anonymous user 1`] = `
`;
exports[`should render the right interface for logged in user 1`] = `
-<li
- className="dropdown js-user-authenticated"
+<Dropdown
+ className="js-user-authenticated"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li
+ className="menu-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li"
>
<a
className="dropdown-toggle navbar-avatar"
href="#"
- onClick={[Function]}
>
<Connect(Avatar)
hash="abcd1234"
@@ -87,59 +133,105 @@ exports[`should render the right interface for logged in user 1`] = `
size={32}
/>
</a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
+</Dropdown>
`;
exports[`should render user organizations 1`] = `
-<li
- className="dropdown js-user-authenticated"
+<Dropdown
+ className="js-user-authenticated"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li
+ className="menu-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li
+ className="divider"
+ role="separator"
+ />
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account/organizations"
+ >
+ my_organizations
+ </Link>
+ </li>
+ <OrganizationListItem
+ organization={
+ Object {
+ "key": "bar",
+ "name": "bar",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <OrganizationListItem
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <OrganizationListItem
+ organization={
+ Object {
+ "key": "myorg",
+ "name": "MyOrg",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <li
+ className="divider"
+ role="separator"
+ />
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+ }
+ tagName="li"
>
<a
className="dropdown-toggle navbar-avatar"
href="#"
- onClick={[Function]}
>
<Connect(Avatar)
hash="abcd1234"
@@ -147,94 +239,5 @@ exports[`should render user organizations 1`] = `
size={32}
/>
</a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li
- className="divider"
- role="separator"
- />
- <li>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account/organizations"
- >
- my_organizations
- </Link>
- </li>
- <OrganizationListItem
- key="bar"
- organization={
- Object {
- "key": "bar",
- "name": "bar",
- "projectVisibility": "public",
- }
- }
- />
- <OrganizationListItem
- key="foo"
- organization={
- Object {
- "key": "foo",
- "name": "Foo",
- "projectVisibility": "public",
- }
- }
- />
- <OrganizationListItem
- key="myorg"
- organization={
- Object {
- "key": "myorg",
- "name": "MyOrg",
- "projectVisibility": "public",
- }
- }
- />
- <li
- className="divider"
- role="separator"
- />
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
+</Dropdown>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
index 8379d1822bb..ff656a311a5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
@@ -91,51 +91,54 @@ export default class SettingsNav extends React.PureComponent<Props> {
extension => extension.key !== 'license/support'
);
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', {
- active:
- open ||
- (!this.isSecurityActive() &&
- !this.isProjectsActive() &&
- !this.isSystemActive() &&
- !this.isSomethingActive(['/admin/extension/license/support']) &&
- !this.isMarketplace())
- })}
- href="#"
- id="settings-navigation-configuration"
- onClick={onToggleClick}>
- {translate('sidebar.project_settings')}
- <i className="icon-dropdown little-spacer-left" />
- </a>
- <ul className="dropdown-menu">
- <li>
- <IndexLink activeClassName="active" to="/admin/settings">
- {translate('settings.page')}
- </IndexLink>
- </li>
- <li>
- <IndexLink activeClassName="active" to="/admin/settings/encryption">
- {translate('property.category.security.encryption')}
- </IndexLink>
- </li>
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ <li>
+ <IndexLink activeClassName="active" to="/admin/settings">
+ {translate('settings.page')}
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink activeClassName="active" to="/admin/settings/encryption">
+ {translate('property.category.security.encryption')}
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink activeClassName="active" to="/admin/custom_metrics">
+ {translate('custom_metrics.page')}
+ </IndexLink>
+ </li>
+ {!organizationsEnabled && (
<li>
- <IndexLink activeClassName="active" to="/admin/custom_metrics">
- {translate('custom_metrics.page')}
+ <IndexLink activeClassName="active" to="/admin/webhooks">
+ {translate('webhooks.page')}
</IndexLink>
</li>
- {!organizationsEnabled && (
- <li>
- <IndexLink activeClassName="active" to="/admin/webhooks">
- {translate('webhooks.page')}
- </IndexLink>
- </li>
- )}
- {extensionsWithoutSupport.map(this.renderExtension)}
- </ul>
- </li>
+ )}
+ {extensionsWithoutSupport.map(this.renderExtension)}
+ </ul>
+ }
+ tagName="li">
+ {({ onToggleClick, open }) => (
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', {
+ active:
+ open ||
+ (!this.isSecurityActive() &&
+ !this.isProjectsActive() &&
+ !this.isSystemActive() &&
+ !this.isSomethingActive(['/admin/extension/license/support']) &&
+ !this.isMarketplace())
+ })}
+ href="#"
+ id="settings-navigation-configuration"
+ onClick={onToggleClick}>
+ {translate('sidebar.project_settings')}
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
)}
</Dropdown>
);
@@ -144,30 +147,33 @@ export default class SettingsNav extends React.PureComponent<Props> {
renderProjectsTab() {
const { organizationsEnabled } = this.props;
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: open || this.isProjectsActive() })}
- href="#"
- onClick={onToggleClick}>
- {translate('sidebar.projects')} <i className="icon-dropdown" />
- </a>
- <ul className="dropdown-menu">
- {!organizationsEnabled && (
- <li>
- <IndexLink activeClassName="active" to="/admin/projects_management">
- {translate('management')}
- </IndexLink>
- </li>
- )}
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ {!organizationsEnabled && (
<li>
- <IndexLink activeClassName="active" to="/admin/background_tasks">
- {translate('background_tasks.page')}
+ <IndexLink activeClassName="active" to="/admin/projects_management">
+ {translate('management')}
</IndexLink>
</li>
- </ul>
- </li>
+ )}
+ <li>
+ <IndexLink activeClassName="active" to="/admin/background_tasks">
+ {translate('background_tasks.page')}
+ </IndexLink>
+ </li>
+ </ul>
+ }
+ tagName="li">
+ {({ onToggleClick, open }) => (
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', { active: open || this.isProjectsActive() })}
+ href="#"
+ onClick={onToggleClick}>
+ {translate('sidebar.projects')} <i className="icon-dropdown" />
+ </a>
)}
</Dropdown>
);
@@ -176,44 +182,47 @@ export default class SettingsNav extends React.PureComponent<Props> {
renderSecurityTab() {
const { organizationsEnabled } = this.props;
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: open || this.isSecurityActive() })}
- href="#"
- onClick={onToggleClick}>
- {translate('sidebar.security')} <i className="icon-dropdown" />
- </a>
- <ul className="dropdown-menu">
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ <li>
+ <IndexLink activeClassName="active" to="/admin/users">
+ {translate('users.page')}
+ </IndexLink>
+ </li>
+ {!organizationsEnabled && (
<li>
- <IndexLink activeClassName="active" to="/admin/users">
- {translate('users.page')}
+ <IndexLink activeClassName="active" to="/admin/groups">
+ {translate('user_groups.page')}
</IndexLink>
</li>
- {!organizationsEnabled && (
- <li>
- <IndexLink activeClassName="active" to="/admin/groups">
- {translate('user_groups.page')}
- </IndexLink>
- </li>
- )}
- {!organizationsEnabled && (
- <li>
- <IndexLink activeClassName="active" to="/admin/permissions">
- {translate('global_permissions.page')}
- </IndexLink>
- </li>
- )}
- {!organizationsEnabled && (
- <li>
- <IndexLink activeClassName="active" to="/admin/permission_templates">
- {translate('permission_templates')}
- </IndexLink>
- </li>
- )}
- </ul>
- </li>
+ )}
+ {!organizationsEnabled && (
+ <li>
+ <IndexLink activeClassName="active" to="/admin/permissions">
+ {translate('global_permissions.page')}
+ </IndexLink>
+ </li>
+ )}
+ {!organizationsEnabled && (
+ <li>
+ <IndexLink activeClassName="active" to="/admin/permission_templates">
+ {translate('permission_templates')}
+ </IndexLink>
+ </li>
+ )}
+ </ul>
+ }
+ tagName="li">
+ {({ onToggleClick, open }) => (
+ <a
+ aria-expanded={String(open)}
+ aria-haspopup="true"
+ className={classNames('dropdown-toggle', { active: open || this.isSecurityActive() })}
+ href="#"
+ onClick={onToggleClick}>
+ {translate('sidebar.security')} <i className="icon-dropdown" />
+ </a>
)}
</Dropdown>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
index ae3f5e900af..256662531cb 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
@@ -33,5 +33,5 @@ it('should work with extensions', () => {
/>
);
expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('Dropdown').map(x => x.dive())).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
index d7495bbbfc3..29a88938c4b 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
@@ -14,9 +14,123 @@ exports[`should work with extensions 1`] = `
</h1>
</header>
<NavBarTabs>
- <Dropdown />
- <Dropdown />
- <Dropdown />
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/settings"
+ >
+ settings.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/settings/encryption"
+ >
+ property.category.security.encryption
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/custom_metrics"
+ >
+ custom_metrics.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/webhooks"
+ >
+ webhooks.page
+ </IndexLink>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/admin/extension/foo"
+ >
+ Foo
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/users"
+ >
+ users.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/groups"
+ >
+ user_groups.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/permissions"
+ >
+ global_permissions.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/permission_templates"
+ >
+ permission_templates
+ </IndexLink>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/projects_management"
+ >
+ management
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/background_tasks"
+ >
+ background_tasks.page
+ </IndexLink>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />
<li>
<IndexLink
activeClassName="active"
@@ -39,154 +153,122 @@ exports[`should work with extensions 1`] = `
exports[`should work with extensions 2`] = `
Array [
- <li
- className="dropdown"
- >
- <a
- className="dropdown-toggle active"
- href="#"
- id="settings-navigation-configuration"
- onClick={[Function]}
- >
- sidebar.project_settings
- <i
- className="icon-dropdown little-spacer-left"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/settings"
- >
- settings.page
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/settings/encryption"
- >
- property.category.security.encryption
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/custom_metrics"
- >
- custom_metrics.page
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/webhooks"
- >
- webhooks.page
- </IndexLink>
- </li>
- <li
- key="foo"
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/admin/extension/foo"
- >
- Foo
- </Link>
- </li>
- </ul>
- </li>,
- <li
- className="dropdown"
- >
- <a
- className="dropdown-toggle"
- href="#"
- onClick={[Function]}
- >
- sidebar.security
-
- <i
- className="icon-dropdown"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/users"
- >
- users.page
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/groups"
- >
- user_groups.page
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/permissions"
- >
- global_permissions.page
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/permission_templates"
- >
- permission_templates
- </IndexLink>
- </li>
- </ul>
- </li>,
- <li
- className="dropdown"
- >
- <a
- className="dropdown-toggle"
- href="#"
- onClick={[Function]}
- >
- sidebar.projects
-
- <i
- className="icon-dropdown"
- />
- </a>
- <ul
- className="dropdown-menu"
- >
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/projects_management"
- >
- management
- </IndexLink>
- </li>
- <li>
- <IndexLink
- activeClassName="active"
- to="/admin/background_tasks"
- >
- background_tasks.page
- </IndexLink>
- </li>
- </ul>
- </li>,
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/settings"
+ >
+ settings.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/settings/encryption"
+ >
+ property.category.security.encryption
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/custom_metrics"
+ >
+ custom_metrics.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/webhooks"
+ >
+ webhooks.page
+ </IndexLink>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/admin/extension/foo"
+ >
+ Foo
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />,
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/users"
+ >
+ users.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/groups"
+ >
+ user_groups.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/permissions"
+ >
+ global_permissions.page
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/permission_templates"
+ >
+ permission_templates
+ </IndexLink>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />,
+ <Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/projects_management"
+ >
+ management
+ </IndexLink>
+ </li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/background_tasks"
+ >
+ background_tasks.page
+ </IndexLink>
+ </li>
+ </ul>
+ }
+ tagName="li"
+ />,
]
`;
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js
index 5912990d256..327844759f8 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.js
+++ b/server/sonar-web/src/main/js/app/components/search/Search.js
@@ -20,7 +20,6 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
-import classNames from 'classnames';
import key from 'keymaster';
import { debounce, keyBy, uniqBy } from 'lodash';
import { FormattedMessage } from 'react-intl';
@@ -31,12 +30,14 @@ import { sortQualifiers } from './utils';
import RecentHistory from '../../components/RecentHistory';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import ClockIcon from '../../../components/common/ClockIcon';
+import OutsideClickHandler from '../../../components/controls/OutsideClickHandler';
import SearchBox from '../../../components/controls/SearchBox';
import { getSuggestions } from '../../../api/components';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { scrollToElement } from '../../../helpers/scrolling';
import { getProjectUrl } from '../../../helpers/urls';
import './Search.css';
+import { DropdownOverlay } from '../../../components/controls/Dropdown';
/*::
type Props = {|
@@ -115,17 +116,22 @@ export default class Search extends React.PureComponent {
componentWillUnmount() {
this.mounted = false;
key.unbind('s');
- window.removeEventListener('click', this.handleClickOutside);
}
- handleClickOutside = (event /*: { target: HTMLElement } */) => {
- if (!this.node || !this.node.contains(event.target)) {
- this.closeSearch(false);
+ handleClickOutside = () => {
+ this.closeSearch(false);
+ };
+
+ handleFocus = () => {
+ // simulate click to close any other dropdowns
+ const body = document.documentElement;
+ if (body) {
+ body.click();
}
+ this.openSearch();
};
openSearch = () => {
- window.addEventListener('click', this.handleClickOutside);
if (!this.state.open && !this.state.query) {
this.search('');
}
@@ -136,7 +142,6 @@ export default class Search extends React.PureComponent {
if (this.input) {
this.input.blur();
}
- window.removeEventListener('click', this.handleClickOutside);
this.setState(
clear
? {
@@ -304,10 +309,6 @@ export default class Search extends React.PureComponent {
this.setState({ selected });
};
- handleClick = (event /*: Event */) => {
- event.stopPropagation();
- };
-
innerRef = (component /*: string */, node /*: HTMLElement */) => {
this.nodes[component] = node;
};
@@ -337,61 +338,65 @@ export default class Search extends React.PureComponent {
);
render() {
- const dropdownClassName = classNames('dropdown', 'navbar-search', { open: this.state.open });
-
- return (
- <li className={dropdownClassName}>
+ const search = (
+ <li className="navbar-search dropdown">
<DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />
<SearchBox
+ autoFocus={this.state.open}
innerRef={this.searchInputRef}
minLength={2}
onChange={this.handleQueryChange}
- onClick={this.handleClick}
- onFocus={this.openSearch}
+ onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
placeholder={translate('search.placeholder')}
value={this.state.query}
/>
{this.state.shortQuery && (
- <span className={classNames('navbar-search-input-hint')}>
+ <span className="navbar-search-input-hint">
{translateWithParameters('select2.tooShort', 2)}
</span>
)}
{this.state.open &&
Object.keys(this.state.results).length > 0 && (
- <div
- className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown"
- ref={node => (this.node = node)}>
- <SearchResults
- allowMore={this.state.query.length !== 1}
- loadingMore={this.state.loadingMore}
- more={this.state.more}
- onMoreClick={this.searchMore}
- onSelect={this.handleSelect}
- renderNoResults={this.renderNoResults}
- renderResult={this.renderResult}
- results={this.state.results}
- selected={this.state.selected}
- />
- <div className="dropdown-bottom-hint">
- <div className="pull-right">
- <ClockIcon className="little-spacer-right" size={12} />
- {translate('recently_browsed')}
- </div>
- <FormattedMessage
- defaultMessage={translate('search.shortcut_hint')}
- id="search.shortcut_hint"
- values={{
- shortcut: <span className="shortcut-button shortcut-button-small">s</span>
- }}
+ <DropdownOverlay noPadding={true}>
+ <div className="global-navbar-search-dropdown" ref={node => (this.node = node)}>
+ <SearchResults
+ allowMore={this.state.query.length !== 1}
+ loadingMore={this.state.loadingMore}
+ more={this.state.more}
+ onMoreClick={this.searchMore}
+ onSelect={this.handleSelect}
+ renderNoResults={this.renderNoResults}
+ renderResult={this.renderResult}
+ results={this.state.results}
+ selected={this.state.selected}
/>
+ <div className="dropdown-bottom-hint">
+ <div className="pull-right">
+ <ClockIcon className="little-spacer-right" size={12} />
+ {translate('recently_browsed')}
+ </div>
+ <FormattedMessage
+ defaultMessage={translate('search.shortcut_hint')}
+ id="search.shortcut_hint"
+ values={{
+ shortcut: <span className="shortcut-button shortcut-button-small">s</span>
+ }}
+ />
+ </div>
</div>
- </div>
+ </DropdownOverlay>
)}
</li>
);
+
+ return this.state.open ? (
+ <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler>
+ ) : (
+ search
+ );
}
}
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.js b/server/sonar-web/src/main/js/app/components/search/SearchResults.js
index c22b619639d..2d8e3b7532f 100644
--- a/server/sonar-web/src/main/js/app/components/search/SearchResults.js
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResults.js
@@ -49,12 +49,12 @@ export default class SearchResults extends React.PureComponent {
const components = this.props.results[qualifier];
if (components.length > 0 && renderedComponents.length > 0) {
- renderedComponents.push(<li key={`divider-${qualifier}`} className="divider" />);
+ renderedComponents.push(<li className="divider" key={`divider-${qualifier}`} />);
}
if (components.length > 0) {
renderedComponents.push(
- <li key={`header-${qualifier}`} className="dropdown-header">
+ <li className="menu-header" key={`header-${qualifier}`}>
{translate('qualifiers', qualifier)}
</li>
);
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
index 2e300a88cda..01168e7a49b 100644
--- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
@@ -94,13 +94,3 @@ it('shows warning about short input', () => {
form.setState({ query: 'foobar x' });
expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
});
-
-it('closes on click outside', () => {
- const form = mount(
- <Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} />
- );
- form.instance().openSearch();
- expect(form.state().open).toBe(true);
- clickOutside();
- expect(form.state().open).toBe(false);
-});
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
index 432b5562168..f13cf142896 100644
--- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
@@ -5,7 +5,7 @@ exports[`renders "Show More" link 1`] = `
className="menu"
>
<li
- className="dropdown-header"
+ className="menu-header"
key="header-TRK"
>
qualifiers.TRK
@@ -34,7 +34,7 @@ exports[`renders "Show More" link 1`] = `
key="divider-BRC"
/>
<li
- className="dropdown-header"
+ className="menu-header"
key="header-BRC"
>
qualifiers.BRC
@@ -57,7 +57,7 @@ exports[`renders different components and dividers between them 1`] = `
className="menu"
>
<li
- className="dropdown-header"
+ className="menu-header"
key="header-TRK"
>
qualifiers.TRK
@@ -77,7 +77,7 @@ exports[`renders different components and dividers between them 1`] = `
key="divider-BRC"
/>
<li
- className="dropdown-header"
+ className="menu-header"
key="header-BRC"
>
qualifiers.BRC
@@ -97,7 +97,7 @@ exports[`renders different components and dividers between them 1`] = `
key="divider-FIL"
/>
<li
- className="dropdown-header"
+ className="menu-header"
key="header-FIL"
>
qualifiers.FIL
diff --git a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css b/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css
deleted file mode 100644
index 031edf980a0..00000000000
--- a/server/sonar-web/src/main/js/app/styles/components/bubble-popup.css
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.bubble-popup {
- position: absolute;
- z-index: var(--bubblePopupZIndex);
- margin-top: -16px;
- margin-left: 8px;
- padding: 10px;
- border: 1px solid var(--barBorderColor);
- border-radius: 3px;
- box-sizing: border-box;
- background-color: #ffffff;
- box-shadow: var(--defaultShadow);
- cursor: default;
-}
-
-.bubble-popup-menu {
- padding: 0;
-}
-
-.bubble-popup-arrow,
-.bubble-popup-arrow:after {
- position: absolute;
- display: block;
- width: 0;
- height: 0;
- border: 8px solid transparent;
-}
-
-.bubble-popup-arrow {
- top: 15px;
- left: -8px;
- border-left-width: 0;
- border-right-color: var(--barBorderColor);
-}
-
-.bubble-popup-arrow:after {
- content: ' ';
- left: 1px;
- bottom: -8px;
- border-left-width: 0;
- border-right-color: #ffffff;
-}
-
-.bubble-popup-bottom {
- margin-top: 8px;
- margin-left: -16px;
-}
-
-.bubble-popup-bottom .bubble-popup-arrow {
- top: -8px;
- left: 15px;
- border-left-width: 8px;
- border-top-width: 0;
- border-right-color: transparent;
- border-bottom-color: var(--barBorderColor);
-}
-
-.bubble-popup-bottom .bubble-popup-arrow:after {
- left: -8px;
- bottom: -9px;
- border-left-width: 8px;
- border-top-width: 0;
- border-right-color: transparent;
- border-bottom-color: #ffffff;
-}
-
-.bubble-popup-bottom-right {
- margin-top: 8px;
- margin-left: -16px;
- margin-left: 0;
- margin-right: -16px;
-}
-
-.bubble-popup-bottom-right .bubble-popup-arrow {
- top: -8px;
- left: 15px;
- border-left-width: 8px;
- border-top-width: 0;
- border-right-color: transparent;
- border-bottom-color: var(--barBorderColor);
-}
-
-.bubble-popup-bottom-right .bubble-popup-arrow:after {
- left: -8px;
- bottom: -9px;
- border-left-width: 8px;
- border-top-width: 0;
- border-right-color: transparent;
- border-bottom-color: #ffffff;
-}
-
-.bubble-popup-bottom-right .bubble-popup-arrow {
- left: auto;
- right: 15px;
-}
-
-.bubble-popup-right {
- margin-left: -8px;
-}
-
-.bubble-popup-right .bubble-popup-arrow {
- right: -8px;
- left: auto;
- border-right-width: 0;
- border-left-width: 8px;
- border-left-color: var(--barBorderColor);
- border-right-color: transparent;
-}
-
-.bubble-popup-right .bubble-popup-arrow:after {
- left: auto;
- right: 1px;
- bottom: -8px;
- border-right-width: 0;
- border-left-width: 8px;
- border-left-color: #ffffff;
- border-right-color: transparent;
-}
-
-.bubble-popup-container {
- max-width: 560px;
- max-height: 300px;
- padding-right: 30px;
- overflow: auto;
-}
-
-.bubble-popup-helper {
- position: relative;
-}
-
-.bubble-popup-helper:focus {
- outline: none;
-}
-
-.bubble-popup-helper-inline {
- display: inline-block;
-}
-
-.bubble-popup-title {
- margin-bottom: 5px;
- font-weight: 600;
-}
-
-.bubble-popup-section {
- width: 450px;
- padding-bottom: 2px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.bubble-popup-section.fluid {
- width: auto;
- max-width: 450px;
-}
-
-.bubble-popup-section + .bubble-popup-section,
-.bubble-popup-section + .bubble-popup-title {
- margin-top: 10px;
-}
-
-.bubble-popup-list {
- margin-top: 5px;
-}
-
-.bubble-popup-list > li {
- padding: 2px 0;
-}
diff --git a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css
index d08b64ea06b..ef8e2bd3f10 100644
--- a/server/sonar-web/src/main/js/app/styles/components/dropdowns.css
+++ b/server/sonar-web/src/main/js/app/styles/components/dropdowns.css
@@ -19,156 +19,12 @@
*/
.dropdown {
position: relative;
-}
-
-.dropdown-toggle:focus {
- outline: 0;
-}
-
-.dropdown-menu {
- min-width: 160px;
- padding: 5px 0;
- list-style: none;
- font-size: var(--smallFontSize);
- text-align: left;
- background-color: #fff;
- position: absolute;
- top: 100%;
- left: 0;
- z-index: var(--dropdownMenuZIndex);
- display: none;
- float: left;
- border: 1px solid var(--barBorderColor);
- background-clip: padding-box;
- box-shadow: var(--defaultShadow);
-}
-
-.dropdown-menu:focus {
- outline: none;
-}
-
-.dropdown-menu > li > a,
-.dropdown-menu > li > span {
- display: block;
- padding: 4px 16px;
- line-height: 16px;
- clear: both;
- font-weight: normal;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.dropdown-menu > li > a {
- color: var(--baseFontColor);
- border-bottom: none;
- transition: none;
-}
-
-.dropdown-menu > li > a.text-danger,
-.dropdown-menu > li > a.text-danger:hover {
- color: var(--red) !important;
-}
-
-.dropdown-menu .divider {
- height: 1px;
- margin: 6px 0;
- overflow: hidden;
- background-color: var(--barBorderColor);
-}
-
-.dropdown-menu > li > a:hover,
-.dropdown-menu > li > a:focus {
- text-decoration: none;
- color: var(--baseFontColor);
- background-color: var(--barBackgroundColor);
-}
-
-.dropdown-menu > .active > a,
-.dropdown-menu > li > .active,
-.dropdown-menu > .active > a:hover,
-.dropdown-menu > li > .active:hover,
-.dropdown-menu > .active > a:focus,
-.dropdown-menu > li > .active:focus {
- color: var(--baseFontColor);
- text-decoration: none;
- outline: 0;
- background-color: var(--barBackgroundColor);
-}
-
-.dropdown-menu .menu-vertically-limited {
- max-height: 300px;
- overflow-y: auto;
-}
-
-.dropdown-menu .menu-footer > a > span {
- border-bottom: 1px solid var(--gray80);
- color: var(--secondFontColor);
-}
-
-.dropdown-menu .menu-footer-note {
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.dropdown-menu .menu-footer.active .menu-footer-note {
- opacity: 1;
-}
-
-.dropdown-menu.pull-right {
- right: 0;
- left: auto;
-}
-
-.open > .dropdown-menu {
- display: block;
-}
-
-.open > a {
- outline: 0;
-}
-
-.dropdown-menu-right {
- left: auto;
- right: 0;
-}
-
-.dropdown-menu-left {
- left: 0;
- right: auto;
-}
-
-.dropdown-header {
- display: block;
- padding: 3px 8px 5px;
- font-size: var(--smallFontSize);
- color: var(--secondFontColor);
- white-space: nowrap;
-}
-
-.dropdown-item {
- padding: 5px 16px;
-}
-
-.dropdown-menu .small-divider {
- height: 1px;
- margin: 4px 20px;
- overflow: hidden;
- background-color: var(--barBackgroundColor);
-}
-
-.dropdown-backdrop {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- top: 0;
- z-index: 990;
+ display: inline-block;
+ vertical-align: middle;
}
.dropdown-bottom-hint {
line-height: 16px;
- margin-top: 5px;
margin-bottom: -5px;
padding: 5px 10px;
border-top: 1px solid var(--barBorderColor);
@@ -176,9 +32,3 @@
color: var(--secondFontColor);
font-size: 11px;
}
-
-.dropdown-item-flex {
- display: flex !important;
- justify-content: space-between;
- align-items: center;
-}
diff --git a/server/sonar-web/src/main/js/app/styles/components/issues.css b/server/sonar-web/src/main/js/app/styles/components/issues.css
index 3ff1e991902..21eb1a3066a 100644
--- a/server/sonar-web/src/main/js/app/styles/components/issues.css
+++ b/server/sonar-web/src/main/js/app/styles/components/issues.css
@@ -168,8 +168,7 @@
}
.issue-changelog {
- min-width: 450px;
- max-width: 540px;
+ width: 450px;
max-height: 320px;
overflow: auto;
white-space: normal;
@@ -345,14 +344,9 @@ input.issue-action-options-search {
.issue-comment-bubble-popup {
width: 440px;
- margin-left: -220px;
font-size: var(--smallFontSize);
}
-.issue-comment-bubble-popup .bubble-popup-arrow {
- left: 220px;
-}
-
.issue-edit-comment-bubble-popup {
width: 440px;
font-size: var(--smallFontSize);
@@ -481,7 +475,7 @@ input.issue-action-options-search {
background-color: #ccc;
}
-.issue .menu:not(.issues-similar-issues-menu) {
+.issue .menu:not(.issues-similar-issues-menu):not(.issue-changelog) {
max-height: 120px;
overflow: auto;
}
diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css
index ac3fd543e5f..e5bf1c05eda 100644
--- a/server/sonar-web/src/main/js/app/styles/components/menu.css
+++ b/server/sonar-web/src/main/js/app/styles/components/menu.css
@@ -31,6 +31,11 @@
outline: none;
}
+.menu.is-container {
+ padding: 5px;
+}
+
+.menu-item,
.menu > li > a,
.menu > li > span {
display: block;
@@ -85,6 +90,11 @@
background-color: var(--barBackgroundColor);
}
+.menu > li > a.text-danger,
+.menu > li > a.text-danger:hover {
+ color: var(--red) !important;
+}
+
.menu .menu-vertically-limited,
.menu.menu-vertically-limited {
max-height: 300px;
@@ -115,7 +125,7 @@
.menu-search {
position: relative;
- padding: 4px 16px 0;
+ padding: var(--gridSize) calc(2 * var(--gridSize)) 0;
}
.menu-search .search-box,
@@ -143,3 +153,15 @@
padding: 4px 16px;
line-height: 16px;
}
+
+.menu-header {
+ padding: var(--gridSize);
+ font-size: 12px;
+ color: #777;
+ white-space: nowrap;
+}
+
+.menu-header:first-child,
+.divider + .menu-header {
+ padding-top: calc(var(--gridSize) - 5px);
+}
diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css
index 9e5a247cdd1..eaef6aa0909 100644
--- a/server/sonar-web/src/main/js/app/styles/sonar.css
+++ b/server/sonar-web/src/main/js/app/styles/sonar.css
@@ -29,7 +29,6 @@
@import './components/ui.css';
@import './components/spinner.css';
@import './components/global-loading.css';
-@import './components/bubble-popup.css';
@import './components/modals.css';
@import './components/alerts.css';
@import './components/issues.css';
diff --git a/server/sonar-web/src/main/js/app/styles/style.css b/server/sonar-web/src/main/js/app/styles/style.css
index 4fc6c561948..61be8927c26 100644
--- a/server/sonar-web/src/main/js/app/styles/style.css
+++ b/server/sonar-web/src/main/js/app/styles/style.css
@@ -200,7 +200,6 @@
}
.property pre,
-.bubble-popup pre,
.coding-rules-detail-parameter pre {
display: inline-block;
min-width: 100%;
@@ -213,7 +212,6 @@
}
.property blockquote,
-.bubble-popup blockquote,
.coding-rules-detail-parameter blockquote {
margin-top: 10px;
padding: 10px;
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 59808a135ba..e57fc075f6c 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -56,6 +56,7 @@ module.exports = {
snippetFontColor: '#f0f0f0',
// sizes
+ grid,
gridSize: `${grid}px`,
baseFontSize: '13px',
@@ -105,5 +106,5 @@ module.exports = {
modalZIndex: '6001',
modalOverlayZIndex: '6000',
- bubblePopupZIndex: '5000'
+ popupZIndex: '5000'
};
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
index 63d039166cc..6b47b2d704d 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import BulkChangeModal from './BulkChangeModal';
import { Query } from '../query';
import { Profile } from '../../../api/quality-profiles';
@@ -40,7 +39,6 @@ interface State {
}
export default class BulkChange extends React.PureComponent<Props, State> {
- closeDropdown?: () => void;
state: State = { modal: false };
getSelectedProfile = () => {
@@ -53,36 +51,24 @@ export default class BulkChange extends React.PureComponent<Props, State> {
handleActivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.setState({ action: 'activate', modal: true, profile: undefined });
};
handleActivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.setState({ action: 'activate', modal: true, profile: this.getSelectedProfile() });
};
handleDeactivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.setState({ action: 'deactivate', modal: true, profile: undefined });
};
handleDeactivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.setState({ action: 'deactivate', modal: true, profile: this.getSelectedProfile() });
};
@@ -105,45 +91,39 @@ export default class BulkChange extends React.PureComponent<Props, State> {
return (
<>
- <Dropdown>
- {({ closeDropdown, onToggleClick, open }) => {
- this.closeDropdown = closeDropdown;
- return (
- <div className={classNames('pull-left dropdown', { open })}>
- <Button className="js-bulk-change" onClick={onToggleClick}>
- {translate('bulk_change')}
- </Button>
- <ul className="dropdown-menu">
+ <Dropdown
+ className="pull-left"
+ overlay={
+ <ul className="menu">
+ <li>
+ <a href="#" onClick={this.handleActivateClick}>
+ {translate('coding_rules.activate_in')}…
+ </a>
+ </li>
+ {allowActivateOnProfile &&
+ profile && (
<li>
- <a href="#" onClick={this.handleActivateClick}>
- {translate('coding_rules.activate_in')}…
+ <a href="#" onClick={this.handleActivateInProfileClick}>
+ {translate('coding_rules.activate_in')} <strong>{profile.name}</strong>
</a>
</li>
- {allowActivateOnProfile &&
- profile && (
- <li>
- <a href="#" onClick={this.handleActivateInProfileClick}>
- {translate('coding_rules.activate_in')} <strong>{profile.name}</strong>
- </a>
- </li>
- )}
+ )}
+ <li>
+ <a href="#" onClick={this.handleDeactivateClick}>
+ {translate('coding_rules.deactivate_in')}…
+ </a>
+ </li>
+ {allowDeactivateOnProfile &&
+ profile && (
<li>
- <a href="#" onClick={this.handleDeactivateClick}>
- {translate('coding_rules.deactivate_in')}…
+ <a href="#" onClick={this.handleDeactivateInProfileClick}>
+ {translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong>
</a>
</li>
- {allowDeactivateOnProfile &&
- profile && (
- <li>
- <a href="#" onClick={this.handleDeactivateInProfileClick}>
- {translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong>
- </a>
- </li>
- )}
- </ul>
- </div>
- );
- }}
+ )}
+ </ul>
+ }>
+ <Button className="js-bulk-change">{translate('bulk_change')}</Button>
</Dropdown>
{this.state.modal &&
this.state.action && (
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index 16e9f0d6f65..60d51aae6bd 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -31,10 +31,11 @@ import DocTooltip from '../../../components/docs/DocTooltip';
import { translate } from '../../../helpers/l10n';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SeverityHelper from '../../../components/shared/SeverityHelper';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import Dropdown from '../../../components/controls/Dropdown';
import TagsList from '../../../components/tags/TagsList';
import DateFormatter from '../../../components/intl/DateFormatter';
import { Button } from '../../../components/ui/buttons';
+import { PopupPlacement } from '../../../components/ui/popups';
interface Props {
canWrite: boolean | undefined;
@@ -46,19 +47,7 @@ interface Props {
ruleDetails: RuleDetails;
}
-interface State {
- tagsPopup: boolean;
-}
-
-export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
- state: State = { tagsPopup: false };
-
- handleTagsClick = () => {
- this.setState(state => ({ tagsPopup: !state.tagsPopup }));
- };
-
- handleTagsPopupToggle = (show: boolean) => this.setState({ tagsPopup: show });
-
+export default class RuleDetailsMeta extends React.PureComponent<Props> {
renderType = () => {
const { ruleDetails } = this.props;
return (
@@ -106,9 +95,10 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
return (
<li className="coding-rules-detail-property" data-meta="tags">
{this.props.canWrite ? (
- <BubblePopupHelper
- isOpen={this.state.tagsPopup}
- popup={
+ <Dropdown
+ closeOnClick={false}
+ closeOnClickOutside={true}
+ overlay={
<RuleDetailsTagsPopup
organization={this.props.organization}
setTags={this.props.onTagsChange}
@@ -116,15 +106,14 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
tags={allTags}
/>
}
- position="bottomleft"
- togglePopup={this.handleTagsPopupToggle}>
- <Button className="button-link" onClick={this.handleTagsClick}>
+ overlayPlacement={PopupPlacement.BottomLeft}>
+ <Button className="button-link">
<TagsList
allowUpdate={canWrite}
tags={allTags.length > 0 ? allTags : [translate('coding_rules.no_tags')]}
/>
</Button>
- </BubblePopupHelper>
+ </Dropdown>
) : (
<TagsList
allowUpdate={canWrite}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
index 38d3520623c..72e0c7a8fa0 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
@@ -21,11 +21,9 @@ import * as React from 'react';
import { without, uniq, difference } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { getRuleTags } from '../../../api/rules';
-import { BubblePopupPosition } from '../../../components/common/BubblePopup';
interface Props {
organization: string | undefined;
- popupPosition?: BubblePopupPosition;
setTags: (tags: string[]) => void;
sysTags: string[];
tags: string[];
@@ -81,7 +79,6 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
- position={this.props.popupPosition || {}}
selectedTags={this.props.tags}
tags={availableTags}
/>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
index 8340279f3ad..f7106afcc46 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import { Query } from '../query';
import { Rule } from '../../../app/types';
import Dropdown from '../../../components/controls/Dropdown';
@@ -31,32 +30,21 @@ interface Props {
}
export default class SimilarRulesFilter extends React.PureComponent<Props> {
- closeDropdown?: () => void;
-
handleLanguageClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.props.onFilterChange({ languages: [this.props.rule.lang] });
};
handleTypeClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
this.props.onFilterChange({ types: [this.props.rule.type] });
};
handleSeverityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
if (this.props.rule.severity) {
this.props.onFilterChange({ severities: [this.props.rule.severity] });
}
@@ -65,9 +53,6 @@ export default class SimilarRulesFilter extends React.PureComponent<Props> {
handleTagClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- if (this.closeDropdown) {
- this.closeDropdown();
- }
const { tag } = event.currentTarget.dataset;
if (tag) {
this.props.onFilterChange({ tags: [tag] });
@@ -80,61 +65,52 @@ export default class SimilarRulesFilter extends React.PureComponent<Props> {
const allTags = [...tags, ...sysTags];
return (
- <Dropdown>
- {({ closeDropdown, onToggleClick, open }) => {
- this.closeDropdown = closeDropdown;
- return (
- <div className={classNames('dropdown display-inline-block', { open })}>
- <a
- className="js-rule-filter link-no-underline spacer-left dropdown-toggle"
- href="#"
- onClick={onToggleClick}>
- <i className="icon-filter icon-half-transparent" />
- <i className="icon-dropdown little-spacer-left" />
- </a>
- <div className="dropdown-menu dropdown-menu-right">
- <header className="dropdown-header">
- {translate('coding_rules.filter_similar_rules')}
- </header>
- <ul className="menu">
- <li>
- <a data-field="language" href="#" onClick={this.handleLanguageClick}>
- {rule.langName}
- </a>
- </li>
+ <Dropdown
+ className="display-inline-block"
+ overlay={
+ <>
+ <ul className="menu">
+ <li className="menu-header">{translate('coding_rules.filter_similar_rules')}</li>
+ <li>
+ <a data-field="language" href="#" onClick={this.handleLanguageClick}>
+ {rule.langName}
+ </a>
+ </li>
+
+ <li>
+ <a data-field="type" href="#" onClick={this.handleTypeClick}>
+ {translate('issue.type', rule.type)}
+ </a>
+ </li>
- <li>
- <a data-field="type" href="#" onClick={this.handleTypeClick}>
- {translate('issue.type', rule.type)}
- </a>
- </li>
+ {severity && (
+ <li>
+ <a data-field="severity" href="#" onClick={this.handleSeverityClick}>
+ <SeverityHelper severity={rule.severity} />
+ </a>
+ </li>
+ )}
- {severity && (
- <li>
- <a data-field="severity" href="#" onClick={this.handleSeverityClick}>
- <SeverityHelper severity={rule.severity} />
+ {allTags.length > 0 && (
+ <>
+ <li className="divider" />
+ {allTags.map(tag => (
+ <li key={tag}>
+ <a data-field="tag" data-tag={tag} href="#" onClick={this.handleTagClick}>
+ <i className="icon-tags icon-half-transparent little-spacer-right" />
+ {tag}
</a>
</li>
- )}
-
- {allTags.length > 0 && (
- <>
- <li className="divider" />
- {allTags.map(tag => (
- <li key={tag}>
- <a data-field="tag" data-tag={tag} href="#" onClick={this.handleTagClick}>
- <i className="icon-tags icon-half-transparent little-spacer-right" />
- {tag}
- </a>
- </li>
- ))}
- </>
- )}
- </ul>
- </div>
- </div>
- );
- }}
+ ))}
+ </>
+ )}
+ </ul>
+ </>
+ }>
+ <a className="js-rule-filter link-no-underline spacer-left dropdown-toggle" href="#">
+ <i className="icon-filter icon-half-transparent" />
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx
deleted file mode 100644
index f4f2ff15d24..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { CustomMeasure } from '../../../app/types';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-interface Props {
- measure: CustomMeasure;
- onDelete: (measureId: string) => Promise<void>;
-}
-
-export default function DeleteButton({ measure, onDelete }: Props) {
- return (
- <ConfirmButton
- confirmButtonText={translate('delete')}
- confirmData={measure.id}
- isDestructive={true}
- modalBody={translateWithParameters(
- 'custom_measures.delete_custom_measure.confirmation',
- measure.metric.name
- )}
- modalHeader={translate('custom_measures.delete_custom_measure')}
- onConfirm={onDelete}>
- {({ onClick }) => (
- <ActionsDropdownItem
- className="js-custom-measure-delete"
- destructive={true}
- onClick={onClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
- </ConfirmButton>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx
new file mode 100644
index 00000000000..f8da5faad95
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteForm.tsx
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { CustomMeasure } from '../../../app/types';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ measure: CustomMeasure;
+ onClose: () => void;
+ onSubmit: () => Promise<void>;
+}
+
+export default function DeleteForm({ measure, onClose, onSubmit }: Props) {
+ const header = translate('custom_measures.delete_custom_measure');
+
+ return (
+ <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters(
+ 'custom_measures.delete_custom_measure.confirmation',
+ measure.metric.name
+ )}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx
deleted file mode 100644
index 655b02b6eaf..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import Form from './Form';
-import { CustomMeasure } from '../../../app/types';
-import { translate } from '../../../helpers/l10n';
-import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-
-interface Props {
- measure: CustomMeasure;
- onEdit: (data: { description: string; id: string; value: string }) => Promise<void>;
-}
-
-interface State {
- modal: boolean;
-}
-
-export default class EditButton extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { modal: false };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleClick = () => {
- this.setState({ modal: true });
- };
-
- handleClose = () => {
- if (this.mounted) {
- this.setState({ modal: false });
- }
- };
-
- handleSubmit = (data: { description: string; value: string }) => {
- return this.props.onEdit({ id: this.props.measure.id, ...data });
- };
-
- render() {
- return (
- <>
- <ActionsDropdownItem className="js-custom-measure-update" onClick={this.handleClick}>
- {translate('update_verb')}
- </ActionsDropdownItem>
- {this.state.modal && (
- <Form
- confirmButtonText={translate('update_verb')}
- header={translate('custom_measures.update_custom_measure')}
- measure={this.props.measure}
- onClose={this.handleClose}
- onSubmit={this.handleSubmit}
- />
- )}
- </>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
new file mode 100644
index 00000000000..335d89faacd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeleteForm from './DeleteForm';
+import Form from './Form';
+import MeasureDate from './MeasureDate';
+import { CustomMeasure } from '../../../app/types';
+import ActionsDropdown, {
+ ActionsDropdownDivider,
+ ActionsDropdownItem
+} from '../../../components/controls/ActionsDropdown';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+interface Props {
+ measure: CustomMeasure;
+ onDelete: (measureId: string) => Promise<void>;
+ onEdit: (data: { description: string; id: string; value: string }) => Promise<void>;
+}
+
+interface State {
+ deleteForm: boolean;
+ editForm: boolean;
+}
+
+export default class Item extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ deleteForm: false,
+ editForm: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleEditClick = () => {
+ this.setState({ editForm: true });
+ };
+
+ handleDeleteClick = () => {
+ this.setState({ deleteForm: true });
+ };
+
+ closeEditForm = () => {
+ if (this.mounted) {
+ this.setState({ editForm: false });
+ }
+ };
+
+ closeDeleteForm = () => {
+ if (this.mounted) {
+ this.setState({ deleteForm: false });
+ }
+ };
+
+ handleEditFormSubmit = (data: { description: string; value: string }) => {
+ return this.props.onEdit({ id: this.props.measure.id, ...data });
+ };
+
+ handleDeleteFormSubmit = () => {
+ return this.props.onDelete(this.props.measure.id);
+ };
+
+ render() {
+ const { measure } = this.props;
+
+ return (
+ <tr data-metric={measure.metric.key}>
+ <td className="nowrap">
+ <div>
+ <span className="js-custom-measure-metric-name">{measure.metric.name}</span>
+ {measure.pending && (
+ <Tooltip overlay={translate('custom_measures.pending_tooltip')}>
+ <span className="js-custom-measure-pending badge badge-warning spacer-left">
+ {translate('custom_measures.pending')}
+ </span>
+ </Tooltip>
+ )}
+ </div>
+ <span className="js-custom-measure-domain note">{measure.metric.domain}</span>
+ </td>
+
+ <td className="nowrap">
+ <strong className="js-custom-measure-value">
+ {formatMeasure(measure.value, measure.metric.type)}
+ </strong>
+ </td>
+
+ <td>
+ <span className="js-custom-measure-description">{measure.description}</span>
+ </td>
+
+ <td>
+ <MeasureDate measure={measure} /> {translate('by_')}{' '}
+ <span className="js-custom-measure-user">{measure.user.name}</span>
+ </td>
+
+ <td className="thin nowrap">
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ className="js-custom-measure-update"
+ onClick={this.handleEditClick}>
+ {translate('update_verb')}
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-custom-measure-delete"
+ destructive={true}
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+
+ {this.state.editForm && (
+ <Form
+ confirmButtonText={translate('update_verb')}
+ header={translate('custom_measures.update_custom_measure')}
+ measure={this.props.measure}
+ onClose={this.closeEditForm}
+ onSubmit={this.handleEditFormSubmit}
+ />
+ )}
+
+ {this.state.deleteForm && (
+ <DeleteForm
+ measure={this.props.measure}
+ onClose={this.closeDeleteForm}
+ onSubmit={this.handleDeleteFormSubmit}
+ />
+ )}
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx
index 239b4eeec6c..f26474192d1 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx
@@ -19,16 +19,9 @@
*/
import * as React from 'react';
import { sortBy } from 'lodash';
-import DeleteButton from './DeleteButton';
-import EditButton from './EditButton';
+import Item from './Item';
import { CustomMeasure } from '../../../app/types';
-import ActionsDropdown, {
- ActionsDropdownDivider
-} from '../../../components/controls/ActionsDropdown';
import { translate } from '../../../helpers/l10n';
-import Tooltip from '../../../components/controls/Tooltip';
-import { formatMeasure } from '../../../helpers/measures';
-import DateFormatter from '../../../components/intl/DateFormatter';
interface Props {
measures: CustomMeasure[];
@@ -52,44 +45,7 @@ export default function List({ measures, onDelete, onEdit }: Props) {
</thead>
<tbody>
{sortBy(measures, measure => measure.metric.name.toLowerCase()).map(measure => (
- <tr data-metric={measure.metric.key} key={measure.id}>
- <td className="nowrap">
- <div>
- <span className="js-custom-measure-metric-name">{measure.metric.name}</span>
- {measure.pending && (
- <Tooltip overlay={translate('custom_measures.pending_tooltip')}>
- <span className="js-custom-measure-pending badge badge-warning spacer-left">
- {translate('custom_measures.pending')}
- </span>
- </Tooltip>
- )}
- </div>
- <span className="js-custom-measure-domain note">{measure.metric.domain}</span>
- </td>
-
- <td className="nowrap">
- <strong className="js-custom-measure-value">
- {formatMeasure(measure.value, measure.metric.type)}
- </strong>
- </td>
-
- <td>
- <span className="js-custom-measure-description">{measure.description}</span>
- </td>
-
- <td>
- <MeasureDate measure={measure} /> {translate('by_')}{' '}
- <span className="js-custom-measure-user">{measure.user.name}</span>
- </td>
-
- <td className="thin nowrap">
- <ActionsDropdown>
- <EditButton measure={measure} onEdit={onEdit} />
- <ActionsDropdownDivider />
- <DeleteButton measure={measure} onDelete={onDelete} />
- </ActionsDropdown>
- </td>
- </tr>
+ <Item key={measure.id} measure={measure} onDelete={onDelete} onEdit={onEdit} />
))}
</tbody>
</table>
@@ -99,27 +55,3 @@ export default function List({ measures, onDelete, onEdit }: Props) {
</div>
);
}
-
-function MeasureDate({ measure }: { measure: CustomMeasure }) {
- if (measure.updatedAt) {
- return (
- <>
- {translate('updated_on')}{' '}
- <span className="js-custom-measure-created-at">
- <DateFormatter date={measure.updatedAt} />
- </span>
- </>
- );
- } else if (measure.createdAt) {
- return (
- <>
- {translate('created_on')}{' '}
- <span className="js-custom-measure-created-at">
- <DateFormatter date={measure.createdAt} />
- </span>
- </>
- );
- } else {
- return <>{translate('created')}</>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx
new file mode 100644
index 00000000000..cde3f66e28d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/MeasureDate.tsx
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { CustomMeasure } from '../../../app/types';
+import DateFormatter from '../../../components/intl/DateFormatter';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ measure: CustomMeasure;
+}
+
+export default function MeasureDate({ measure }: Props) {
+ if (measure.updatedAt) {
+ return (
+ <>
+ {translate('updated_on')}{' '}
+ <span className="js-custom-measure-created-at">
+ <DateFormatter date={measure.updatedAt} />
+ </span>
+ </>
+ );
+ } else if (measure.createdAt) {
+ return (
+ <>
+ {translate('created_on')}{' '}
+ <span className="js-custom-measure-created-at">
+ <DateFormatter date={measure.createdAt} />
+ </span>
+ </>
+ );
+ } else {
+ return <>{translate('created')}</>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx
index 3c032dafc62..411e81c2d7e 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteForm-test.tsx
@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import DeleteButton from '../DeleteButton';
+import DeleteForm from '../DeleteForm';
-it('should delete custom measure', () => {
+it('should render', () => {
const measure = {
createdAt: '2017-01-01',
description: 'my custom measure',
@@ -31,10 +31,7 @@ it('should delete custom measure', () => {
user: { active: true, login: 'user', name: 'user' },
value: 'custom-value'
};
- const onDelete = jest.fn();
- const wrapper = shallow(<DeleteButton measure={measure} onDelete={onDelete} />);
- expect(wrapper).toMatchSnapshot();
-
- wrapper.find('ConfirmButton').prop<Function>('onConfirm')('1');
- expect(onDelete).toBeCalledWith('1');
+ expect(
+ shallow(<DeleteForm measure={measure} onClose={jest.fn()} onSubmit={jest.fn()} />).dive()
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
index f1efbc3a527..16942dfd967 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
@@ -19,27 +19,31 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import EditButton from '../EditButton';
+import Item from '../Item';
import { click } from '../../../../helpers/testUtils';
+const measure = {
+ createdAt: '2017-01-01',
+ description: 'my custom measure',
+ id: '1',
+ metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
+ projectKey: 'foo',
+ user: { active: true, login: 'user', name: 'user' },
+ value: 'custom-value'
+};
+
+it('should render', () => {
+ expect(
+ shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} />)
+ ).toMatchSnapshot();
+});
+
it('should edit metric', () => {
- const measure = {
- createdAt: '2017-01-01',
- description: 'my custom measure',
- id: '1',
- metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
- projectKey: 'foo',
- user: { active: true, login: 'user', name: 'user' },
- value: 'custom-value'
- };
const onEdit = jest.fn();
-
- const wrapper = shallow(<EditButton measure={measure} onEdit={onEdit} />);
- expect(wrapper).toMatchSnapshot();
+ const wrapper = shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={onEdit} />);
click(wrapper.find('.js-custom-measure-update'));
wrapper.update();
- expect(wrapper).toMatchSnapshot();
wrapper.find('Form').prop<Function>('onSubmit')({
...measure,
@@ -48,3 +52,14 @@ it('should edit metric', () => {
});
expect(onEdit).toBeCalledWith({ ...measure, description: 'new-description', value: 'new-value' });
});
+
+it('should delete custom measure', () => {
+ const onDelete = jest.fn();
+ const wrapper = shallow(<Item measure={measure} onDelete={onDelete} onEdit={jest.fn()} />);
+
+ click(wrapper.find('.js-custom-measure-delete'));
+ wrapper.update();
+
+ wrapper.find('DeleteForm').prop<Function>('onSubmit')();
+ expect(onDelete).toBeCalledWith('1');
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap
deleted file mode 100644
index c9fc5439615..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should delete custom measure 1`] = `
-<ConfirmButton
- confirmButtonText="delete"
- confirmData="1"
- isDestructive={true}
- modalBody="custom_measures.delete_custom_measure.confirmation.custom-metric"
- modalHeader="custom_measures.delete_custom_measure"
- onConfirm={[MockFunction]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
new file mode 100644
index 00000000000..77d8286a180
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<Modal
+ contentLabel="custom_measures.delete_custom_measure"
+ onRequestClose={[MockFunction]}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ custom_measures.delete_custom_measure
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ custom_measures.delete_custom_measure.confirmation.custom-metric
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <SubmitButton
+ className="button-red"
+ disabled={false}
+ >
+ delete
+ </SubmitButton>
+ <ResetButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap
deleted file mode 100644
index bd0a8035083..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap
+++ /dev/null
@@ -1,48 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should edit metric 1`] = `
-<React.Fragment>
- <ActionsDropdownItem
- className="js-custom-measure-update"
- onClick={[Function]}
- >
- update_verb
- </ActionsDropdownItem>
-</React.Fragment>
-`;
-
-exports[`should edit metric 2`] = `
-<React.Fragment>
- <ActionsDropdownItem
- className="js-custom-measure-update"
- onClick={[Function]}
- >
- update_verb
- </ActionsDropdownItem>
- <Form
- confirmButtonText="update_verb"
- header="custom_measures.update_custom_measure"
- measure={
- Object {
- "createdAt": "2017-01-01",
- "description": "my custom measure",
- "id": "1",
- "metric": Object {
- "key": "custom",
- "name": "custom-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "custom-value",
- }
- }
- onClose={[Function]}
- onSubmit={[Function]}
- />
-</React.Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
new file mode 100644
index 00000000000..225ca0c587c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<tr
+ data-metric="custom"
+>
+ <td
+ className="nowrap"
+ >
+ <div>
+ <span
+ className="js-custom-measure-metric-name"
+ >
+ custom-metric
+ </span>
+ </div>
+ <span
+ className="js-custom-measure-domain note"
+ />
+ </td>
+ <td
+ className="nowrap"
+ >
+ <strong
+ className="js-custom-measure-value"
+ >
+ custom-value
+ </strong>
+ </td>
+ <td>
+ <span
+ className="js-custom-measure-description"
+ >
+ my custom measure
+ </span>
+ </td>
+ <td>
+ <MeasureDate
+ measure={
+ Object {
+ "createdAt": "2017-01-01",
+ "description": "my custom measure",
+ "id": "1",
+ "metric": Object {
+ "key": "custom",
+ "name": "custom-metric",
+ "type": "STRING",
+ },
+ "projectKey": "foo",
+ "user": Object {
+ "active": true,
+ "login": "user",
+ "name": "user",
+ },
+ "value": "custom-value",
+ }
+ }
+ />
+
+ by_
+
+ <span
+ className="js-custom-measure-user"
+ >
+ user
+ </span>
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ className="js-custom-measure-update"
+ onClick={[Function]}
+ >
+ update_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-custom-measure-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap
index 005bedff1e0..14d2a0c362d 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap
@@ -26,235 +26,53 @@ exports[`should render 1`] = `
</tr>
</thead>
<tbody>
- <tr
- data-metric="another"
+ <Item
key="2"
- >
- <td
- className="nowrap"
- >
- <div>
- <span
- className="js-custom-measure-metric-name"
- >
- another-metric
- </span>
- </div>
- <span
- className="js-custom-measure-domain note"
- />
- </td>
- <td
- className="nowrap"
- >
- <strong
- className="js-custom-measure-value"
- >
- another-value
- </strong>
- </td>
- <td>
- <span
- className="js-custom-measure-description"
- />
- </td>
- <td>
- <MeasureDate
- measure={
- Object {
- "createdAt": "2017-01-01",
- "id": "2",
- "metric": Object {
- "key": "another",
- "name": "another-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "another-value",
- }
- }
- />
-
- by_
-
- <span
- className="js-custom-measure-user"
- >
- user
- </span>
- </td>
- <td
- className="thin nowrap"
- >
- <ActionsDropdown>
- <EditButton
- measure={
- Object {
- "createdAt": "2017-01-01",
- "id": "2",
- "metric": Object {
- "key": "another",
- "name": "another-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "another-value",
- }
- }
- onEdit={[MockFunction]}
- />
- <ActionsDropdownDivider />
- <DeleteButton
- measure={
- Object {
- "createdAt": "2017-01-01",
- "id": "2",
- "metric": Object {
- "key": "another",
- "name": "another-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "another-value",
- }
- }
- onDelete={[MockFunction]}
- />
- </ActionsDropdown>
- </td>
- </tr>
- <tr
- data-metric="custom"
+ measure={
+ Object {
+ "createdAt": "2017-01-01",
+ "id": "2",
+ "metric": Object {
+ "key": "another",
+ "name": "another-metric",
+ "type": "STRING",
+ },
+ "projectKey": "foo",
+ "user": Object {
+ "active": true,
+ "login": "user",
+ "name": "user",
+ },
+ "value": "another-value",
+ }
+ }
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
+ <Item
key="1"
- >
- <td
- className="nowrap"
- >
- <div>
- <span
- className="js-custom-measure-metric-name"
- >
- custom-metric
- </span>
- </div>
- <span
- className="js-custom-measure-domain note"
- />
- </td>
- <td
- className="nowrap"
- >
- <strong
- className="js-custom-measure-value"
- >
- custom-value
- </strong>
- </td>
- <td>
- <span
- className="js-custom-measure-description"
- >
- my custom measure
- </span>
- </td>
- <td>
- <MeasureDate
- measure={
- Object {
- "createdAt": "2017-01-01",
- "description": "my custom measure",
- "id": "1",
- "metric": Object {
- "key": "custom",
- "name": "custom-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "custom-value",
- }
- }
- />
-
- by_
-
- <span
- className="js-custom-measure-user"
- >
- user
- </span>
- </td>
- <td
- className="thin nowrap"
- >
- <ActionsDropdown>
- <EditButton
- measure={
- Object {
- "createdAt": "2017-01-01",
- "description": "my custom measure",
- "id": "1",
- "metric": Object {
- "key": "custom",
- "name": "custom-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "custom-value",
- }
- }
- onEdit={[MockFunction]}
- />
- <ActionsDropdownDivider />
- <DeleteButton
- measure={
- Object {
- "createdAt": "2017-01-01",
- "description": "my custom measure",
- "id": "1",
- "metric": Object {
- "key": "custom",
- "name": "custom-metric",
- "type": "STRING",
- },
- "projectKey": "foo",
- "user": Object {
- "active": true,
- "login": "user",
- "name": "user",
- },
- "value": "custom-value",
- }
- }
- onDelete={[MockFunction]}
- />
- </ActionsDropdown>
- </td>
- </tr>
+ measure={
+ Object {
+ "createdAt": "2017-01-01",
+ "description": "my custom measure",
+ "id": "1",
+ "metric": Object {
+ "key": "custom",
+ "name": "custom-metric",
+ "type": "STRING",
+ },
+ "projectKey": "foo",
+ "user": Object {
+ "active": true,
+ "login": "user",
+ "name": "user",
+ },
+ "value": "custom-value",
+ }
+ }
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
</tbody>
</table>
</div>
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx
deleted file mode 100644
index d825823c7f3..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteButton.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { Metric } from '../../../app/types';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-interface Props {
- metric: Metric;
- onDelete: (metricKey: string) => Promise<void>;
-}
-
-export default function DeleteButton({ metric, onDelete }: Props) {
- return (
- <ConfirmButton
- confirmButtonText={translate('delete')}
- confirmData={metric.key}
- isDestructive={true}
- modalBody={translateWithParameters('custom_metrics.delete_metric.confirmation', metric.name)}
- modalHeader={translate('custom_metrics.delete_metric')}
- onConfirm={onDelete}>
- {({ onClick }) => (
- <ActionsDropdownItem className="js-metric-delete" destructive={true} onClick={onClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
- </ConfirmButton>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx
new file mode 100644
index 00000000000..c799bcb1a56
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/DeleteForm.tsx
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Metric } from '../../../app/types';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ metric: Metric;
+ onClose: () => void;
+ onSubmit: () => Promise<void>;
+}
+
+export default function DeleteForm({ metric, onClose, onSubmit }: Props) {
+ const header = translate('custom_metrics.delete_metric');
+
+ return (
+ <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters('custom_metrics.delete_metric.confirmation', metric.name)}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx
deleted file mode 100644
index d0870fa8c06..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/EditButton.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import Form, { MetricProps } from './Form';
-import { Metric } from '../../../app/types';
-import { translate } from '../../../helpers/l10n';
-import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-
-interface Props {
- domains: string[];
- metric: Metric;
- onEdit: (data: { id: string } & MetricProps) => Promise<void>;
- types: string[];
-}
-
-interface State {
- modal: boolean;
-}
-
-export default class EditButton extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { modal: false };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleClick = () => {
- this.setState({ modal: true });
- };
-
- handleClose = () => {
- if (this.mounted) {
- this.setState({ modal: false });
- }
- };
-
- handleSubmit = (data: MetricProps) => {
- return this.props.onEdit({ id: this.props.metric.id, ...data });
- };
-
- render() {
- return (
- <>
- <ActionsDropdownItem className="js-metric-update" onClick={this.handleClick}>
- {translate('update_details')}
- </ActionsDropdownItem>
- {this.state.modal && (
- <Form
- confirmButtonText={translate('update_verb')}
- domains={this.props.domains}
- header={translate('custom_metrics.update_metric')}
- metric={this.props.metric}
- onClose={this.handleClose}
- onSubmit={this.handleSubmit}
- types={this.props.types}
- />
- )}
- </>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx
new file mode 100644
index 00000000000..7f31c648bf3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeleteForm from './DeleteForm';
+import Form, { MetricProps } from './Form';
+import { Metric } from '../../../app/types';
+import ActionsDropdown, {
+ ActionsDropdownDivider,
+ ActionsDropdownItem
+} from '../../../components/controls/ActionsDropdown';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ domains?: string[];
+ metric: Metric;
+ onDelete: (metricKey: string) => Promise<void>;
+ onEdit: (data: { id: string } & MetricProps) => Promise<void>;
+ types?: string[];
+}
+
+interface State {
+ deleteForm: boolean;
+ editForm: boolean;
+}
+
+export default class Item extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ state: State = { deleteForm: false, editForm: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleEditClick = () => {
+ this.setState({ editForm: true });
+ };
+
+ handleDeleteClick = () => {
+ this.setState({ deleteForm: true });
+ };
+
+ closeEditForm = () => {
+ if (this.mounted) {
+ this.setState({ editForm: false });
+ }
+ };
+
+ closeDeleteForm = () => {
+ if (this.mounted) {
+ this.setState({ deleteForm: false });
+ }
+ };
+
+ handleEditFormSubmit = (data: MetricProps) => {
+ return this.props.onEdit({ id: this.props.metric.id, ...data });
+ };
+
+ handleDeleteFormSubmit = () => {
+ return this.props.onDelete(this.props.metric.key);
+ };
+
+ render() {
+ const { domains, metric, types } = this.props;
+
+ return (
+ <tr data-metric={metric.key}>
+ <td className="width-30">
+ <div>
+ <strong className="js-metric-name">{metric.name}</strong>
+ <span className="js-metric-key note little-spacer-left">{metric.key}</span>
+ </div>
+ </td>
+
+ <td className="width-20">
+ <span className="js-metric-domain">{metric.domain}</span>
+ </td>
+
+ <td className="width-20">
+ <span className="js-metric-type">{translate('metric.type', metric.type)}</span>
+ </td>
+
+ <td className="width-20" title={metric.description}>
+ <span className="js-metric-description">{metric.description}</span>
+ </td>
+
+ <td className="thin nowrap">
+ <ActionsDropdown>
+ {domains &&
+ types && (
+ <ActionsDropdownItem className="js-metric-update" onClick={this.handleEditClick}>
+ {translate('update_details')}
+ </ActionsDropdownItem>
+ )}
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-metric-delete"
+ destructive={true}
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+
+ {this.state.editForm &&
+ domains &&
+ types && (
+ <Form
+ confirmButtonText={translate('update_verb')}
+ domains={domains}
+ header={translate('custom_metrics.update_metric')}
+ metric={metric}
+ onClose={this.closeEditForm}
+ onSubmit={this.handleEditFormSubmit}
+ types={types}
+ />
+ )}
+
+ {this.state.deleteForm && (
+ <DeleteForm
+ metric={metric}
+ onClose={this.closeDeleteForm}
+ onSubmit={this.handleDeleteFormSubmit}
+ />
+ )}
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx
index a5449bf68b6..ddbf383ec2d 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/List.tsx
@@ -19,13 +19,9 @@
*/
import * as React from 'react';
import { sortBy } from 'lodash';
-import DeleteButton from './DeleteButton';
-import EditButton from './EditButton';
import { MetricProps } from './Form';
+import Item from './Item';
import { Metric } from '../../../app/types';
-import ActionsDropdown, {
- ActionsDropdownDivider
-} from '../../../components/controls/ActionsDropdown';
import { translate } from '../../../helpers/l10n';
interface Props {
@@ -43,42 +39,14 @@ export default function List({ domains, metrics, onDelete, onEdit, types }: Prop
<table className="data zebra zebra-hover">
<tbody>
{sortBy(metrics, metric => metric.name.toLowerCase()).map(metric => (
- <tr data-metric={metric.key} key={metric.key}>
- <td className="width-30">
- <div>
- <strong className="js-metric-name">{metric.name}</strong>
- <span className="js-metric-key note little-spacer-left">{metric.key}</span>
- </div>
- </td>
-
- <td className="width-20">
- <span className="js-metric-domain">{metric.domain}</span>
- </td>
-
- <td className="width-20">
- <span className="js-metric-type">{translate('metric.type', metric.type)}</span>
- </td>
-
- <td className="width-20" title={metric.description}>
- <span className="js-metric-description">{metric.description}</span>
- </td>
-
- <td className="thin nowrap">
- <ActionsDropdown>
- {domains &&
- types && (
- <EditButton
- domains={domains}
- metric={metric}
- onEdit={onEdit}
- types={types}
- />
- )}
- <ActionsDropdownDivider />
- <DeleteButton metric={metric} onDelete={onDelete} />
- </ActionsDropdown>
- </td>
- </tr>
+ <Item
+ domains={domains}
+ key={metric.key}
+ metric={metric}
+ onDelete={onDelete}
+ onEdit={onEdit}
+ types={types}
+ />
))}
</tbody>
</table>
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx
index a35e39a610e..bbfe2d0f2b1 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/DeleteForm-test.tsx
@@ -19,14 +19,11 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import DeleteButton from '../DeleteButton';
+import DeleteForm from '../DeleteForm';
-it('should delete metric', () => {
+it('should render', () => {
const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' };
- const onDelete = jest.fn();
- const wrapper = shallow(<DeleteButton metric={metric} onDelete={onDelete} />);
- expect(wrapper).toMatchSnapshot();
-
- wrapper.find('ConfirmButton').prop<Function>('onConfirm')('foo');
- expect(onDelete).toBeCalledWith('foo');
+ expect(
+ shallow(<DeleteForm metric={metric} onClose={jest.fn()} onSubmit={jest.fn()} />).dive()
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx
index 5fce34493a4..dc731037d09 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/EditButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/Item-test.tsx
@@ -19,26 +19,32 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import EditButton from '../EditButton';
+import Item from '../Item';
import { click } from '../../../../helpers/testUtils';
+const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' };
+
+it('should render', () => {
+ expect(
+ shallow(<Item metric={metric} onDelete={jest.fn()} onEdit={jest.fn()} />)
+ ).toMatchSnapshot();
+});
+
it('should edit metric', () => {
- const metric = { id: '3', key: 'foo', name: 'Foo', type: 'INT' };
const onEdit = jest.fn();
const wrapper = shallow(
- <EditButton
+ <Item
domains={['Coverage', 'Issues']}
metric={metric}
+ onDelete={jest.fn()}
onEdit={onEdit}
types={['INT', 'STRING']}
/>
);
- expect(wrapper).toMatchSnapshot();
click(wrapper.find('.js-metric-update'));
wrapper.update();
- expect(wrapper).toMatchSnapshot();
wrapper.find('Form').prop<Function>('onSubmit')({
...metric,
@@ -47,3 +53,14 @@ it('should edit metric', () => {
});
expect(onEdit).toBeCalledWith({ ...metric, description: 'bla bla', domain: 'Coverage' });
});
+
+it('should delete metric', () => {
+ const onDelete = jest.fn();
+ const wrapper = shallow(<Item metric={metric} onDelete={onDelete} onEdit={jest.fn()} />);
+
+ click(wrapper.find('.js-metric-delete'));
+ wrapper.update();
+
+ wrapper.find('DeleteForm').prop<Function>('onSubmit')();
+ expect(onDelete).toBeCalledWith('foo');
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap
deleted file mode 100644
index bd6e9cd420b..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should delete metric 1`] = `
-<ConfirmButton
- confirmButtonText="delete"
- confirmData="foo"
- isDestructive={true}
- modalBody="custom_metrics.delete_metric.confirmation.Foo"
- modalHeader="custom_metrics.delete_metric"
- onConfirm={[MockFunction]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
new file mode 100644
index 00000000000..cd0903c2c8a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<Modal
+ contentLabel="custom_metrics.delete_metric"
+ onRequestClose={[MockFunction]}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ custom_metrics.delete_metric
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ custom_metrics.delete_metric.confirmation.Foo
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <SubmitButton
+ className="button-red"
+ disabled={false}
+ >
+ delete
+ </SubmitButton>
+ <ResetButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap
deleted file mode 100644
index 66d078530d4..00000000000
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/EditButton-test.tsx.snap
+++ /dev/null
@@ -1,49 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should edit metric 1`] = `
-<React.Fragment>
- <ActionsDropdownItem
- className="js-metric-update"
- onClick={[Function]}
- >
- update_details
- </ActionsDropdownItem>
-</React.Fragment>
-`;
-
-exports[`should edit metric 2`] = `
-<React.Fragment>
- <ActionsDropdownItem
- className="js-metric-update"
- onClick={[Function]}
- >
- update_details
- </ActionsDropdownItem>
- <Form
- confirmButtonText="update_verb"
- domains={
- Array [
- "Coverage",
- "Issues",
- ]
- }
- header="custom_metrics.update_metric"
- metric={
- Object {
- "id": "3",
- "key": "foo",
- "name": "Foo",
- "type": "INT",
- }
- }
- onClose={[Function]}
- onSubmit={[Function]}
- types={
- Array [
- "INT",
- "STRING",
- ]
- }
- />
-</React.Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap
new file mode 100644
index 00000000000..dbef9673e8e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Item-test.tsx.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<tr
+ data-metric="foo"
+>
+ <td
+ className="width-30"
+ >
+ <div>
+ <strong
+ className="js-metric-name"
+ >
+ Foo
+ </strong>
+ <span
+ className="js-metric-key note little-spacer-left"
+ >
+ foo
+ </span>
+ </div>
+ </td>
+ <td
+ className="width-20"
+ >
+ <span
+ className="js-metric-domain"
+ />
+ </td>
+ <td
+ className="width-20"
+ >
+ <span
+ className="js-metric-type"
+ >
+ metric.type.INT
+ </span>
+ </td>
+ <td
+ className="width-20"
+ >
+ <span
+ className="js-metric-description"
+ />
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-metric-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap
index 1fb174588c7..31507c7f042 100644
--- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/List-test.tsx.snap
@@ -9,133 +9,33 @@ exports[`should render 1`] = `
className="data zebra zebra-hover"
>
<tbody>
- <tr
- data-metric="bar"
+ <Item
key="bar"
- >
- <td
- className="width-30"
- >
- <div>
- <strong
- className="js-metric-name"
- >
- Bar
- </strong>
- <span
- className="js-metric-key note little-spacer-left"
- >
- bar
- </span>
- </div>
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-domain"
- >
- Coverage
- </span>
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-type"
- >
- metric.type.INT
- </span>
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-description"
- />
- </td>
- <td
- className="thin nowrap"
- >
- <ActionsDropdown>
- <ActionsDropdownDivider />
- <DeleteButton
- metric={
- Object {
- "domain": "Coverage",
- "id": "4",
- "key": "bar",
- "name": "Bar",
- "type": "INT",
- }
- }
- onDelete={[MockFunction]}
- />
- </ActionsDropdown>
- </td>
- </tr>
- <tr
- data-metric="foo"
+ metric={
+ Object {
+ "domain": "Coverage",
+ "id": "4",
+ "key": "bar",
+ "name": "Bar",
+ "type": "INT",
+ }
+ }
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
+ <Item
key="foo"
- >
- <td
- className="width-30"
- >
- <div>
- <strong
- className="js-metric-name"
- >
- Foo
- </strong>
- <span
- className="js-metric-key note little-spacer-left"
- >
- foo
- </span>
- </div>
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-domain"
- />
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-type"
- >
- metric.type.INT
- </span>
- </td>
- <td
- className="width-20"
- >
- <span
- className="js-metric-description"
- />
- </td>
- <td
- className="thin nowrap"
- >
- <ActionsDropdown>
- <ActionsDropdownDivider />
- <DeleteButton
- metric={
- Object {
- "id": "3",
- "key": "foo",
- "name": "Foo",
- "type": "INT",
- }
- }
- onDelete={[MockFunction]}
- />
- </ActionsDropdown>
- </td>
- </tr>
+ metric={
+ Object {
+ "id": "3",
+ "key": "foo",
+ "name": "Foo",
+ "type": "INT",
+ }
+ }
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
</tbody>
</table>
</div>
diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx
new file mode 100644
index 00000000000..a1a3a495677
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteForm.tsx
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Group } from '../../../app/types';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ group: Group;
+ onClose: () => void;
+ onSubmit: () => Promise<void>;
+}
+
+export default function DeleteForm({ group, onClose, onSubmit }: Props) {
+ const header = translate('groups.delete_group');
+
+ return (
+ <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters('groups.delete_group.confirmation', group.name)}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx
deleted file mode 100644
index 2de9ded54a1..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import Form from './Form';
-import { Group } from '../../../app/types';
-import { translate } from '../../../helpers/l10n';
-import { omitNil } from '../../../helpers/request';
-
-interface Props {
- children: (props: { onClick: () => void }) => React.ReactNode;
- group: Group;
- onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>;
-}
-
-interface State {
- modal: boolean;
-}
-
-export default class EditGroup extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { modal: false };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleClick = () => {
- this.setState({ modal: true });
- };
-
- handleClose = () => {
- if (this.mounted) {
- this.setState({ modal: false });
- }
- };
-
- handleSubmit = ({ name, description }: { name: string; description: string }) => {
- const { group } = this.props;
- return this.props.onEdit({
- description,
- id: group.id,
- // pass `name` only if it has changed, otherwise the WS fails
- ...omitNil({ name: name !== group.name ? name : undefined })
- });
- };
-
- render() {
- return (
- <>
- {this.props.children({ onClick: this.handleClick })}
- {this.state.modal && (
- <Form
- confirmButtonText={translate('update_verb')}
- group={this.props.group}
- header={translate('groups.update_group')}
- onClose={this.handleClose}
- onSubmit={this.handleSubmit}
- />
- )}
- </>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
index 651066f64b0..a1f2f8ba611 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
@@ -18,15 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import EditGroup from './EditGroup';
+import DeleteForm from './DeleteForm';
import EditMembers from './EditMembers';
+import Form from './Form';
import { Group } from '../../../app/types';
import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
} from '../../../components/controls/ActionsDropdown';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
+import { omitNil } from '../../../helpers/request';
interface Props {
group: Group;
@@ -36,11 +37,57 @@ interface Props {
organization: string | undefined;
}
-export default class ListItem extends React.PureComponent<Props> {
- handleDelete = () => {
+interface State {
+ deleteForm: boolean;
+ editForm: boolean;
+}
+
+export default class ListItem extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { deleteForm: false, editForm: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDeleteClick = () => {
+ this.setState({ deleteForm: true });
+ };
+
+ handleEditClick = () => {
+ this.setState({ editForm: true });
+ };
+
+ closeDeleteForm = () => {
+ if (this.mounted) {
+ this.setState({ deleteForm: false });
+ }
+ };
+
+ closeEditForm = () => {
+ if (this.mounted) {
+ this.setState({ editForm: false });
+ }
+ };
+
+ handleDeleteFormSubmit = () => {
return this.props.onDelete(this.props.group.name);
};
+ handleEditFormSubmit = ({ name, description }: { name: string; description: string }) => {
+ const { group } = this.props;
+ return this.props.onEdit({
+ description,
+ id: group.id,
+ // pass `name` only if it has changed, otherwise the WS fails
+ ...omitNil({ name: name !== group.name ? name : undefined })
+ });
+ };
+
render() {
const { group } = this.props;
@@ -71,32 +118,37 @@ export default class ListItem extends React.PureComponent<Props> {
<td className="thin nowrap text-right">
{!group.default && (
<ActionsDropdown>
- <EditGroup group={group} onEdit={this.props.onEdit}>
- {({ onClick }) => (
- <ActionsDropdownItem className="js-group-update" onClick={onClick}>
- {translate('update_details')}
- </ActionsDropdownItem>
- )}
- </EditGroup>
+ <ActionsDropdownItem className="js-group-update" onClick={this.handleEditClick}>
+ {translate('update_details')}
+ </ActionsDropdownItem>
<ActionsDropdownDivider />
- <ConfirmButton
- confirmButtonText={translate('delete')}
- isDestructive={true}
- modalBody={translateWithParameters('groups.delete_group.confirmation', group.name)}
- modalHeader={translate('groups.delete_group')}
- onConfirm={this.handleDelete}>
- {({ onClick }) => (
- <ActionsDropdownItem
- className="js-group-delete"
- destructive={true}
- onClick={onClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
- </ConfirmButton>
+ <ActionsDropdownItem
+ className="js-group-delete"
+ destructive={true}
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
</ActionsDropdown>
)}
</td>
+
+ {this.state.deleteForm && (
+ <DeleteForm
+ group={group}
+ onClose={this.closeDeleteForm}
+ onSubmit={this.handleDeleteFormSubmit}
+ />
+ )}
+
+ {this.state.editForm && (
+ <Form
+ confirmButtonText={translate('update_verb')}
+ group={group}
+ header={translate('groups.update_group')}
+ onClose={this.closeEditForm}
+ onSubmit={this.handleEditFormSubmit}
+ />
+ )}
</tr>
);
}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx
index 750dd8a89a3..9aa65bc38a9 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopup-test.js
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx
@@ -17,20 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import * as React from 'react';
import { shallow } from 'enzyme';
-import React from 'react';
-import BubblePopup from '../BubblePopup';
+import DeleteForm from '../DeleteForm';
-const props = {
- position: { top: 0, right: 0 },
- customClass: 'custom'
-};
-
-it('should render popup', () => {
- const popup = shallow(
- <BubblePopup {...props}>
- <span>test</span>
- </BubblePopup>
- );
- expect(popup).toMatchSnapshot();
+it('should render', () => {
+ const group = { id: 3, name: 'Foo', membersCount: 5 };
+ expect(
+ shallow(<DeleteForm group={group} onClose={jest.fn()} onSubmit={jest.fn()} />).dive()
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx
deleted file mode 100644
index d650c321233..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import EditGroup from '../EditGroup';
-
-it('should edit group', () => {
- const group = { id: 3, name: 'Foo', membersCount: 5 };
- const onEdit = jest.fn();
- const newDescription = 'bla bla';
- let onClick: any;
-
- const wrapper = shallow(
- <EditGroup group={group} onEdit={onEdit}>
- {props => {
- ({ onClick } = props);
- return <div />;
- }}
- </EditGroup>
- );
- expect(wrapper).toMatchSnapshot();
-
- onClick();
- wrapper.update();
- expect(wrapper).toMatchSnapshot();
-
- // change name
- wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: newDescription });
- expect(onEdit).lastCalledWith({ description: newDescription, id: 3, name: 'Bar' });
-
- // change description
- wrapper.find('Form').prop<Function>('onSubmit')({
- name: group.name,
- description: newDescription
- });
- expect(onEdit).lastCalledWith({ description: newDescription, id: group.id });
-
- wrapper.find('Form').prop<Function>('onClose')();
- wrapper.update();
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
index 28c9bb1617a..805c151b5cb 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
@@ -20,6 +20,27 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ListItem from '../ListItem';
+import { click } from '../../../../helpers/testUtils';
+
+it('should edit group', () => {
+ const group = { id: 3, name: 'Foo', membersCount: 5 };
+ const onEdit = jest.fn();
+ const wrapper = shallow(
+ <ListItem
+ group={group}
+ onDelete={jest.fn()}
+ onEdit={onEdit}
+ onEditMembers={jest.fn()}
+ organization="org"
+ />
+ );
+
+ click(wrapper.find('.js-group-update'));
+ wrapper.update();
+
+ wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: 'bla bla' });
+ expect(onEdit).lastCalledWith({ description: 'bla bla', id: 3, name: 'Bar' });
+});
it('should delete group', () => {
const group = { id: 3, name: 'Foo', membersCount: 5 };
@@ -35,7 +56,10 @@ it('should delete group', () => {
);
expect(wrapper).toMatchSnapshot();
- wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ click(wrapper.find('.js-group-delete'));
+ wrapper.update();
+
+ wrapper.find('DeleteForm').prop<Function>('onSubmit')();
expect(onDelete).toBeCalledWith('Foo');
});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
new file mode 100644
index 00000000000..4d93bc7cf40
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<Modal
+ contentLabel="groups.delete_group"
+ onRequestClose={[MockFunction]}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ groups.delete_group
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ groups.delete_group.confirmation.Foo
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <SubmitButton
+ className="button-red"
+ disabled={false}
+ >
+ delete
+ </SubmitButton>
+ <ResetButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap
deleted file mode 100644
index 43bc8b832cb..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap
+++ /dev/null
@@ -1,32 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should edit group 1`] = `
-<React.Fragment>
- <div />
-</React.Fragment>
-`;
-
-exports[`should edit group 2`] = `
-<React.Fragment>
- <div />
- <Form
- confirmButtonText="update_verb"
- group={
- Object {
- "id": 3,
- "membersCount": 5,
- "name": "Foo",
- }
- }
- header="groups.update_group"
- onClose={[Function]}
- onSubmit={[Function]}
- />
-</React.Fragment>
-`;
-
-exports[`should edit group 3`] = `
-<React.Fragment>
- <div />
-</React.Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
index 2146de899da..59d94b8e22b 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
@@ -48,24 +48,20 @@ exports[`should delete group 1`] = `
className="thin nowrap text-right"
>
<ActionsDropdown>
- <EditGroup
- group={
- Object {
- "id": 3,
- "membersCount": 5,
- "name": "Foo",
- }
- }
- onEdit={[MockFunction]}
- />
+ <ActionsDropdownItem
+ className="js-group-update"
+ onClick={[Function]}
+ >
+ update_details
+ </ActionsDropdownItem>
<ActionsDropdownDivider />
- <ConfirmButton
- confirmButtonText="delete"
- isDestructive={true}
- modalBody="groups.delete_group.confirmation.Foo"
- modalHeader="groups.delete_group"
- onConfirm={[Function]}
- />
+ <ActionsDropdownItem
+ className="js-group-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
</ActionsDropdown>
</td>
</tr>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index df3a3a1374c..95d0e0dacea 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import Helmet from 'react-helmet';
-import * as classNames from 'classnames';
import * as key from 'keymaster';
import { keyBy, union, without } from 'lodash';
import * as PropTypes from 'prop-types';
@@ -804,27 +803,26 @@ export default class App extends React.PureComponent<Props, State> {
thirdState={thirdState}
/>
{checked.length > 0 ? (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('dropdown display-inline-block', { open })}>
- <Button id="issues-bulk-change" onClick={onToggleClick}>
- {translate('bulk_change')}
- <i className="icon-dropdown little-spacer-left" />
- </Button>
- <ul className="dropdown-menu">
- <li>
- <a href="#" onClick={this.handleBulkChangeClick}>
- {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)}
- </a>
- </li>
- <li>
- <a href="#" onClick={this.handleBulkChangeSelectedClick}>
- {translateWithParameters('issues.bulk_change_selected', checked.length)}
- </a>
- </li>
- </ul>
- </div>
- )}
+ <Dropdown
+ className="display-inline-block"
+ overlay={
+ <ul className="menu">
+ <li>
+ <a href="#" onClick={this.handleBulkChangeClick}>
+ {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)}
+ </a>
+ </li>
+ <li>
+ <a href="#" onClick={this.handleBulkChangeSelectedClick}>
+ {translateWithParameters('issues.bulk_change_selected', checked.length)}
+ </a>
+ </li>
+ </ul>
+ }>
+ <Button id="issues-bulk-change">
+ {translate('bulk_change')}
+ <i className="icon-dropdown little-spacer-left" />
+ </Button>
</Dropdown>
) : (
<Button id="issues-bulk-change" onClick={this.handleBulkChangeClick}>
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
index 26b333cc334..080399bbe71 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
@@ -19,36 +19,32 @@
*/
import * as React from 'react';
import PluginChangeLogItem from './PluginChangeLogItem';
-import BubblePopup from '../../../components/common/BubblePopup';
import { Release, Update } from '../../../api/plugins';
import { translate } from '../../../helpers/l10n';
interface Props {
- popupPosition?: any;
release: Release;
update: Update;
}
-export default function PluginChangeLog({ popupPosition, release, update }: Props) {
+export default function PluginChangeLog({ release, update }: Props) {
return (
- <BubblePopup position={popupPosition} customClass="bubble-popup-bottom-right">
- <div className="abs-width-300 bubble-popup-container">
- <div className="bubble-popup-title">{translate('changelog')}</div>
- <ul className="js-plugin-changelog-list">
- {update.previousUpdates &&
- update.previousUpdates.map(
- previousUpdate =>
- previousUpdate.release ? (
- <PluginChangeLogItem
- key={previousUpdate.release.version}
- release={previousUpdate.release}
- update={previousUpdate}
- />
- ) : null
- )}
- <PluginChangeLogItem release={release} update={update} />
- </ul>
- </div>
- </BubblePopup>
+ <div className="abs-width-300">
+ <h6>{translate('changelog')}</h6>
+ <ul className="js-plugin-changelog-list">
+ {update.previousUpdates &&
+ update.previousUpdates.map(
+ previousUpdate =>
+ previousUpdate.release ? (
+ <PluginChangeLogItem
+ key={previousUpdate.release.version}
+ release={previousUpdate.release}
+ update={previousUpdate}
+ />
+ ) : null
+ )}
+ <PluginChangeLogItem release={release} update={update} />
+ </ul>
+ </div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
index b542908b5c2..a2ff350334b 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import PluginChangeLog from './PluginChangeLog';
import { Release, Update } from '../../../api/plugins';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import Dropdown from '../../../components/controls/Dropdown';
import { Button } from '../../../components/ui/buttons';
interface Props {
@@ -28,39 +28,12 @@ interface Props {
update: Update;
}
-interface State {
- changelogOpen: boolean;
-}
-
-export default class PluginChangeLogButton extends React.PureComponent<Props, State> {
- state: State = { changelogOpen: false };
-
- toggleChangelog = (show?: boolean) => {
- if (show !== undefined) {
- this.setState({ changelogOpen: show });
- } else {
- this.setState(state => ({ changelogOpen: !state.changelogOpen }));
- }
- };
-
- handleClick = () => {
- this.toggleChangelog();
- };
-
- render() {
- return (
- <div className="display-inline-block little-spacer-left">
- <Button
- className="button-link js-changelog issue-rule icon-ellipsis-h"
- onClick={this.handleClick}
- />
- <BubblePopupHelper
- isOpen={this.state.changelogOpen}
- popup={<PluginChangeLog release={this.props.release} update={this.props.update} />}
- position="bottomright"
- togglePopup={this.toggleChangelog}
- />
- </div>
- );
- }
+export default function PluginChangeLogButton({ release, update }: Props) {
+ return (
+ <Dropdown
+ className="display-inline-block little-spacer-left"
+ overlay={<PluginChangeLog release={release} update={update} />}>
+ <Button className="button-link js-changelog issue-rule icon-ellipsis-h" />
+ </Dropdown>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
index 891502d42e6..792f218ffac 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
@@ -22,10 +22,11 @@ import React from 'react';
import RemoveMemberForm from './forms/RemoveMemberForm';
import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm';
import Avatar from '../../../components/ui/Avatar';
-import { translateWithParameters } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import ActionsDropdown, {
- ActionsDropdownDivider
+ ActionsDropdownDivider,
+ ActionsDropdownItem
} from '../../../components/controls/ActionsDropdown';
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
@@ -38,12 +39,47 @@ type Props = {
removeMember: Member => void,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
};
+
+type State = {
+ removeMemberForm: bool,
+ manageGroupsForm: bool
+}
*/
const AVATAR_SIZE /*: number */ = 36;
export default class MembersListItem extends React.PureComponent {
+ mounted /*: bool */ = false;
/*:: props: Props; */
+ state /*: State */ = { removeMemberForm: false, manageGroupsForm: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleManageGroupsClick = () => {
+ this.setState({ manageGroupsForm: true });
+ };
+
+ closeManageGroupsForm = () => {
+ if (this.mounted) {
+ this.setState({ manageGroupsForm: false });
+ }
+ };
+
+ handleRemoveMemberClick = () => {
+ this.setState({ removeMemberForm: true });
+ };
+
+ closeRemoveMemberForm = () => {
+ if (this.mounted) {
+ this.setState({ removeMemberForm: false });
+ }
+ };
render() {
const { member, organization } = this.props;
@@ -65,22 +101,38 @@ export default class MembersListItem extends React.PureComponent {
</td>
)}
{organization.canAdmin && (
- <td className="nowrap text-middle text-right">
- <ActionsDropdown>
+ <React.Fragment>
+ <td className="nowrap text-middle text-right">
+ <ActionsDropdown>
+ <ActionsDropdownItem onClick={this.handleManageGroupsClick}>
+ {translate('organization.members.manage_groups')}
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem destructive={true} onClick={this.handleRemoveMemberClick}>
+ {translate('organization.members.remove')}
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+
+ {this.state.manageGroupsForm && (
<ManageMemberGroupsForm
- organizationGroups={this.props.organizationGroups}
+ member={this.props.member}
+ onClose={this.closeManageGroupsForm}
organization={this.props.organization}
+ organizationGroups={this.props.organizationGroups}
updateMemberGroups={this.props.updateMemberGroups}
- member={this.props.member}
/>
- <ActionsDropdownDivider />
+ )}
+
+ {this.state.removeMemberForm && (
<RemoveMemberForm
+ member={this.props.member}
+ onClose={this.closeRemoveMemberForm}
organization={this.props.organization}
removeMember={this.props.removeMember}
- member={this.props.member}
/>
- </ActionsDropdown>
- </td>
+ )}
+ </React.Fragment>
)}
</tr>
);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
index b88cbc6b739..2f04b608477 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
@@ -28,45 +28,26 @@ exports[`should groups at 0 if the groupCount field is not defined (just added u
>
organization.members.x_groups.0
</td>
- <td
- className="nowrap text-middle text-right"
- >
- <ActionsDropdown>
- <ManageMemberGroupsForm
- member={
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "login": "john",
- "name": "John Doe",
- }
- }
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <ActionsDropdownDivider />
- <RemoveMemberForm
- member={
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "login": "john",
- "name": "John Doe",
- }
- }
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- </ActionsDropdown>
- </td>
+ <React.Fragment>
+ <td
+ className="nowrap text-middle text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ organization.members.manage_groups
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ destructive={true}
+ onClick={[Function]}
+ >
+ organization.members.remove
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+ </React.Fragment>
</tr>
`;
@@ -124,46 +105,25 @@ exports[`should render actions and groups for admin 1`] = `
>
organization.members.x_groups.3
</td>
- <td
- className="nowrap text-middle text-right"
- >
- <ActionsDropdown>
- <ManageMemberGroupsForm
- member={
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- }
- }
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <ActionsDropdownDivider />
- <RemoveMemberForm
- member={
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- }
- }
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- </ActionsDropdown>
- </td>
+ <React.Fragment>
+ <td
+ className="nowrap text-middle text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ organization.members.manage_groups
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ destructive={true}
+ onClick={[Function]}
+ >
+ organization.members.remove
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+ </React.Fragment>
</tr>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
index 3c748a3588c..4339538beb1 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
@@ -24,12 +24,12 @@ import { getUserGroups } from '../../../../api/users';
import Modal from '../../../../components/controls/Modal';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox';
-import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
/*:: import type { Organization, OrgGroup } from '../../../../store/organizations/duck'; */
/*::
type Props = {
+ onClose: () => void;
member: Member,
organization: Organization,
organizationGroups: Array<OrgGroup>,
@@ -39,7 +39,6 @@ type Props = {
/*::
type State = {
- open: boolean,
userGroups?: {},
loading?: boolean
};
@@ -48,28 +47,17 @@ type State = {
export default class ManageMemberGroupsForm extends React.PureComponent {
/*:: mounted: boolean */
/*:: props: Props; */
-
- state /*: State */ = {
- open: false
- };
+ state /*: State */ = {};
componentDidMount() {
this.mounted = true;
+ this.loadUserGroups();
}
componentWillUnmount() {
this.mounted = false;
}
- openForm = () => {
- this.loadUserGroups();
- this.setState({ open: true });
- };
-
- closeForm = () => {
- this.setState({ open: false });
- };
-
loadUserGroups = () => {
this.setState({ loading: true });
getUserGroups(this.props.member.login, this.props.organization.key).then(
@@ -119,13 +107,13 @@ export default class ManageMemberGroupsForm extends React.PureComponent {
Object.keys(pickBy(this.state.userGroups, group => group.status === 'add')),
Object.keys(pickBy(this.state.userGroups, group => group.status === 'remove'))
);
- this.closeForm();
+ this.props.onClose();
};
- renderModal() {
+ render() {
const header = translate('organization.members.manage_groups');
return (
- <Modal key="manage-member-modal" contentLabel={header} onRequestClose={this.closeForm}>
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
@@ -142,9 +130,9 @@ export default class ManageMemberGroupsForm extends React.PureComponent {
<ul className="list-spaced">
{this.props.organizationGroups.map(group => (
<OrganizationGroupCheckbox
- key={group.id}
- group={group}
checked={this.isGroupSelected(group.name)}
+ group={group}
+ key={group.id}
onCheck={this.onCheck}
/>
))}
@@ -154,7 +142,7 @@ export default class ManageMemberGroupsForm extends React.PureComponent {
<footer className="modal-foot">
<div>
<button type="submit">{translate('save')}</button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
{translate('cancel')}
</button>
</div>
@@ -163,16 +151,4 @@ export default class ManageMemberGroupsForm extends React.PureComponent {
</Modal>
);
}
-
- render() {
- const buttonComponent = (
- <ActionsDropdownItem onClick={this.openForm}>
- {translate('organization.members.manage_groups')}
- </ActionsDropdownItem>
- );
- if (this.state.open) {
- return [buttonComponent, this.renderModal()];
- }
- return buttonComponent;
- }
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
index d4dae2fa781..343054e6be3 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
@@ -20,69 +20,48 @@
// @flow
import React from 'react';
import Modal from '../../../../components/controls/Modal';
-import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
/*:: import type { Organization } from '../../../../store/organizations/duck'; */
/*::
type Props = {
+ onClose: () => void;
member: Member,
organization: Organization,
removeMember: (member: Member) => void
};
*/
-/*::
-type State = {
- open: boolean
-};
-*/
-
export default class RemoveMemberForm extends React.PureComponent {
/*:: props: Props; */
-
- state /*: State */ = {
- open: false
- };
-
- openForm = () => {
- this.setState({ open: true });
- };
-
- closeForm = () => {
- this.setState({ open: false });
- };
-
handleSubmit = (e /*: Object */) => {
e.preventDefault();
this.props.removeMember(this.props.member);
- this.closeForm();
+ this.props.onClose();
};
- renderModal() {
+ render() {
const header = translate('users.remove');
return (
- <Modal key="remove-member-modal" contentLabel={header} onRequestClose={this.closeForm}>
+ <Modal contentLabel={header} key="remove-member-modal" onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<form onSubmit={this.handleSubmit}>
- <div className="modal-body markdown">
- <p>
- {translateWithParameters(
- 'organization.members.remove_x',
- this.props.member.name,
- this.props.organization.name
- )}
- </p>
+ <div className="modal-body">
+ {translateWithParameters(
+ 'organization.members.remove_x',
+ this.props.member.name,
+ this.props.organization.name
+ )}
</div>
<footer className="modal-foot">
<div>
- <button type="submit" className="button-red" autoFocus={true}>
+ <button autoFocus={true} className="button-red" type="submit">
{translate('remove')}
</button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
{translate('cancel')}
</button>
</div>
@@ -91,16 +70,4 @@ export default class RemoveMemberForm extends React.PureComponent {
</Modal>
);
}
-
- render() {
- const buttonComponent = (
- <ActionsDropdownItem destructive={true} onClick={this.openForm}>
- {translate('organization.members.remove')}
- </ActionsDropdownItem>
- );
- if (this.state.open) {
- return [buttonComponent, this.renderModal()];
- }
- return buttonComponent;
- }
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js
index 2bd5905184b..c2d30153c74 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js
@@ -54,19 +54,19 @@ function getMountedForm(updateFunc = jest.fn()) {
const wrapper = shallow(
<ManageMemberGroupsForm
member={member}
+ onClose={jest.fn()}
organization={organization}
organizationGroups={organizationGroups}
updateMemberGroups={updateFunc}
- />
+ />,
+ { disableLifecycleMethods: true }
);
const instance = wrapper.instance();
- instance.loadUserGroups = jest.fn(() => {
- instance.setState({ loading: false, userGroups });
- });
+ wrapper.setState({ loading: false, userGroups });
return { wrapper, instance };
}
-it('should render and open the modal', () => {
+it('should render', () => {
const wrapper = shallow(
<ManageMemberGroupsForm
member={member}
@@ -76,21 +76,10 @@ it('should render and open the modal', () => {
/>
);
expect(wrapper).toMatchSnapshot();
- wrapper.setState({ open: true });
- expect(wrapper.first().getElements()).toMatchSnapshot();
-});
-
-it('should correctly handle user interactions', () => {
- const form = getMountedForm();
- form.wrapper.find('ActionsDropdownItem').prop('onClick')();
- expect(form.wrapper.state('open')).toBeTruthy();
- expect(form.instance.loadUserGroups).toBeCalled();
- expect(form.wrapper.state()).toMatchSnapshot();
});
it('should correctly select the groups', () => {
const form = getMountedForm();
- form.instance.openForm(mockEvent);
expect(form.instance.isGroupSelected(11)).toBeTruthy();
expect(form.instance.isGroupSelected(7)).toBeFalsy();
form.instance.onCheck(11, false);
@@ -103,21 +92,9 @@ it('should correctly select the groups', () => {
it('should correctly handle the submit event and close the modal', () => {
const updateMemberGroups = jest.fn();
const form = getMountedForm(updateMemberGroups);
- form.instance.openForm(mockEvent);
form.instance.onCheck(11, false);
form.instance.onCheck(7, true);
form.instance.handleSubmit(mockEvent);
expect(updateMemberGroups.mock.calls).toMatchSnapshot();
expect(form.wrapper.state()).toMatchSnapshot();
});
-
-it('should reset the selected groups when the modal is opened', () => {
- const form = getMountedForm();
- form.instance.openForm(mockEvent);
- form.instance.onCheck(11, false);
- form.instance.onCheck(7, true);
- expect(form.wrapper.state()).toMatchSnapshot();
- form.instance.closeForm();
- form.instance.openForm(mockEvent);
- expect(form.wrapper.state()).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js
index e02af92237a..fca12f0187b 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js
@@ -22,30 +22,31 @@ import { shallow } from 'enzyme';
import { click, mockEvent } from '../../../../../helpers/testUtils';
import RemoveMemberForm from '../RemoveMemberForm';
-jest.mock('react-dom');
-
const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
const organization = { name: 'MyOrg' };
-it('should render and open the modal', () => {
+it('should render ', () => {
const wrapper = shallow(
- <RemoveMemberForm member={member} removeMember={jest.fn()} organization={organization} />
+ <RemoveMemberForm member={member} organization={organization} removeMember={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
- wrapper.setState({ open: true });
- expect(wrapper.first().getElements()).toMatchSnapshot();
});
it('should correctly handle user interactions', () => {
const removeMember = jest.fn();
const wrapper = shallow(
- <RemoveMemberForm member={member} removeMember={removeMember} organization={organization} />
+ <RemoveMemberForm
+ member={member}
+ onClose={jest.fn()}
+ organization={organization}
+ removeMember={removeMember}
+ />
);
- const instance = wrapper.instance();
- wrapper.find('ActionsDropdownItem').prop('onClick')();
- expect(wrapper.state('open')).toBeTruthy();
- instance.handleSubmit(mockEvent);
- expect(removeMember.mock.calls).toMatchSnapshot();
- instance.closeForm();
- expect(wrapper.state('open')).toBeFalsy();
+ wrapper.instance().handleSubmit(mockEvent);
+ expect(removeMember).toBeCalledWith({
+ avatar: '',
+ groupCount: 3,
+ login: 'admin',
+ name: 'Admin Istrator'
+ });
});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap
index 94ff9bb6705..2eea5b66085 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap
@@ -22,7 +22,6 @@ Array [
exports[`should correctly handle the submit event and close the modal 2`] = `
Object {
"loading": false,
- "open": false,
"userGroups": Object {
"11": Object {
"description": "Technical accounts",
@@ -38,21 +37,6 @@ Object {
}
`;
-exports[`should correctly handle user interactions 1`] = `
-Object {
- "loading": false,
- "open": true,
- "userGroups": Object {
- "11": Object {
- "description": "Technical accounts",
- "id": 11,
- "name": "pull-request-analysers",
- "selected": true,
- },
- },
-}
-`;
-
exports[`should correctly select the groups 1`] = `
Object {
"11": Object {
@@ -68,136 +52,48 @@ Object {
}
`;
-exports[`should render and open the modal 1`] = `
-<ActionsDropdownItem
- onClick={[Function]}
+exports[`should render 1`] = `
+<Modal
+ contentLabel="organization.members.manage_groups"
>
- organization.members.manage_groups
-</ActionsDropdownItem>
-`;
-
-exports[`should render and open the modal 2`] = `
-Array [
- <ActionsDropdownItem
- onClick={[Function]}
+ <header
+ className="modal-head"
>
- organization.members.manage_groups
- </ActionsDropdownItem>,
- <Modal
- contentLabel="organization.members.manage_groups"
- onRequestClose={[Function]}
+ <h2>
+ organization.members.manage_groups
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
>
- <header
- className="modal-head"
+ <div
+ className="modal-body"
>
- <h2>
- organization.members.manage_groups
- </h2>
- </header>
- <form
- onSubmit={[Function]}
+ <strong>
+ organization.members.members_groups.Admin Istrator
+ </strong>
+
+ <i
+ className="spinner"
+ />
+ </div>
+ <footer
+ className="modal-foot"
>
- <div
- className="modal-body"
- >
- <strong>
- organization.members.members_groups.Admin Istrator
- </strong>
-
- <ul
- className="list-spaced"
+ <div>
+ <button
+ type="submit"
+ >
+ save
+ </button>
+ <button
+ className="button-link"
+ type="reset"
>
- <OrganizationGroupCheckbox
- checked={false}
- group={
- Object {
- "description": "",
- "id": "7",
- "membersCount": 12,
- "name": "professionals",
- }
- }
- onCheck={[Function]}
- />
- <OrganizationGroupCheckbox
- checked={false}
- group={
- Object {
- "description": "Technical accounts",
- "id": "11",
- "membersCount": 3,
- "name": "pull-request-analysers",
- }
- }
- onCheck={[Function]}
- />
- <OrganizationGroupCheckbox
- checked={false}
- group={
- Object {
- "description": "System administrators",
- "id": "1",
- "membersCount": 17,
- "name": "sonar-administrators",
- }
- }
- onCheck={[Function]}
- />
- </ul>
+ cancel
+ </button>
</div>
- <footer
- className="modal-foot"
- >
- <div>
- <button
- type="submit"
- >
- save
- </button>
- <button
- className="button-link"
- onClick={[Function]}
- type="reset"
- >
- cancel
- </button>
- </div>
- </footer>
- </form>
- </Modal>,
-]
-`;
-
-exports[`should reset the selected groups when the modal is opened 1`] = `
-Object {
- "loading": false,
- "open": true,
- "userGroups": Object {
- "11": Object {
- "description": "Technical accounts",
- "id": 11,
- "name": "pull-request-analysers",
- "selected": true,
- "status": "remove",
- },
- "7": Object {
- "status": "add",
- },
- },
-}
-`;
-
-exports[`should reset the selected groups when the modal is opened 2`] = `
-Object {
- "loading": false,
- "open": true,
- "userGroups": Object {
- "11": Object {
- "description": "Technical accounts",
- "id": 11,
- "name": "pull-request-analysers",
- "selected": true,
- },
- },
-}
+ </footer>
+ </form>
+</Modal>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap
index 861e8d98f02..eb235ae0e4a 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap
@@ -1,77 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should correctly handle user interactions 1`] = `
-Array [
- Array [
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- },
- ],
-]
-`;
-
-exports[`should render and open the modal 1`] = `
-<ActionsDropdownItem
- destructive={true}
- onClick={[Function]}
+exports[`should render 1`] = `
+<Modal
+ contentLabel="users.remove"
+ key="remove-member-modal"
>
- organization.members.remove
-</ActionsDropdownItem>
-`;
-
-exports[`should render and open the modal 2`] = `
-Array [
- <ActionsDropdownItem
- destructive={true}
- onClick={[Function]}
+ <header
+ className="modal-head"
>
- organization.members.remove
- </ActionsDropdownItem>,
- <Modal
- contentLabel="users.remove"
- onRequestClose={[Function]}
+ <h2>
+ users.remove
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
>
- <header
- className="modal-head"
+ <div
+ className="modal-body"
>
- <h2>
- users.remove
- </h2>
- </header>
- <form
- onSubmit={[Function]}
+ organization.members.remove_x.Admin Istrator.MyOrg
+ </div>
+ <footer
+ className="modal-foot"
>
- <div
- className="modal-body markdown"
- >
- <p>
- organization.members.remove_x.Admin Istrator.MyOrg
- </p>
+ <div>
+ <button
+ autoFocus={true}
+ className="button-red"
+ type="submit"
+ >
+ remove
+ </button>
+ <button
+ className="button-link"
+ type="reset"
+ >
+ cancel
+ </button>
</div>
- <footer
- className="modal-foot"
- >
- <div>
- <button
- autoFocus={true}
- className="button-red"
- type="submit"
- >
- remove
- </button>
- <button
- className="button-link"
- onClick={[Function]}
- type="reset"
- >
- cancel
- </button>
- </div>
- </footer>
- </form>
- </Modal>,
-]
+ </footer>
+ </form>
+</Modal>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
deleted file mode 100644
index fad51874d3d..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.organization-switch {
- display: inline-block;
-}
-
-.organization-switch .dropdown-toggle {
- display: flex;
- align-items: center;
- height: calc(4 * var(--gridSize));
- padding: 0 var(--gridSize);
- border: 1px solid transparent;
- border-radius: 2px;
- box-sizing: border-box;
- color: var(--baseFontColor) !important;
- transition: all 0.3s ease;
-}
-
-.organization-switch .dropdown-toggle:hover,
-.organization-switch.open .dropdown-toggle {
- border-color: var(--barBorderColor);
- background-color: #fff;
- box-shadow: var(--defaultShadow);
-}
-
-.organization-switch.open .dropdown-toggle {
- border-bottom-color: transparent;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-.organization-switch .dropdown-menu {
- min-width: 100%;
- margin-left: -1px;
-}
-
-.organization-switch .dropdown-menu > li > a {
- padding-left: var(--gridSize);
- padding-right: var(--gridSize);
-}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
index 636dc6cf0a1..f9b1e0ddf81 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
@@ -24,7 +24,6 @@ import OrganizationNavigationMenu from './OrganizationNavigationMenu';
import * as theme from '../../../app/theme';
import ContextNavBar from '../../../components/nav/ContextNavBar';
import { Organization } from '../../../app/types';
-import './OrganizationNavigation.css';
interface Props {
location: { pathname: string };
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
index 338494e7604..3b6f30e664f 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
@@ -48,71 +48,69 @@ export default function OrganizationNavigationAdministration({ location, organiz
);
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active: adminActive })}
- id="organization-navigation-admin"
- href="#"
- onClick={onToggleClick}>
- {translate('layout.settings')}
- <DropdownIcon className="little-spacer-left" />
- </a>
- <ul className="dropdown-menu">
- {extensions.map(extension => (
- <li key={extension.key}>
- <Link
- to={`/organizations/${organization.key}/extension/${extension.key}`}
- activeClassName="active">
- {extension.name}
- </Link>
- </li>
- ))}
- <li>
- <Link to={`/organizations/${organization.key}/groups`} activeClassName="active">
- {translate('user_groups.page')}
- </Link>
- </li>
- <li>
- <Link to={`/organizations/${organization.key}/permissions`} activeClassName="active">
- {translate('permissions.page')}
- </Link>
- </li>
- <li>
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ {extensions.map(extension => (
+ <li key={extension.key}>
<Link
- to={`/organizations/${organization.key}/permission_templates`}
- activeClassName="active">
- {translate('permission_templates')}
- </Link>
- </li>
- <li>
- <Link
- to={`/organizations/${organization.key}/projects_management`}
- activeClassName="active">
- {translate('projects_management')}
- </Link>
- </li>
- <li>
- <Link to={`/organizations/${organization.key}/webhooks`} activeClassName="active">
- {translate('webhooks.page')}
+ activeClassName="active"
+ to={`/organizations/${organization.key}/extension/${extension.key}`}>
+ {extension.name}
</Link>
</li>
+ ))}
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/groups`}>
+ {translate('user_groups.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/permissions`}>
+ {translate('permissions.page')}
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ to={`/organizations/${organization.key}/permission_templates`}>
+ {translate('permission_templates')}
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ to={`/organizations/${organization.key}/projects_management`}>
+ {translate('projects_management')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/webhooks`}>
+ {translate('webhooks.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/edit`}>
+ {translate('edit')}
+ </Link>
+ </li>
+ {organization.canDelete && (
<li>
- <Link to={`/organizations/${organization.key}/edit`} activeClassName="active">
- {translate('edit')}
+ <Link activeClassName="active" to={`/organizations/${organization.key}/delete`}>
+ {translate('delete')}
</Link>
</li>
- {organization.canDelete && (
- <li>
- <Link to={`/organizations/${organization.key}/delete`} activeClassName="active">
- {translate('delete')}
- </Link>
- </li>
- )}
- </ul>
- </li>
- )}
+ )}
+ </ul>
+ }
+ tagName="li">
+ <a
+ className={classNames('dropdown-toggle', { active: adminActive })}
+ href="#"
+ id="organization-navigation-admin">
+ {translate('layout.settings')}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx
index 5151194ba9f..67eac961218 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationExtensions.tsx
@@ -23,6 +23,7 @@ import * as classNames from 'classnames';
import { Organization } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
import Dropdown from '../../../components/controls/Dropdown';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
interface Props {
location: { pathname: string };
@@ -40,31 +41,28 @@ export default function OrganizationNavigationExtensions({ location, organizatio
);
return (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <li className={classNames('dropdown', { open })}>
- <a
- className={classNames('dropdown-toggle', { active })}
- id="organization-navigation-more"
- href="#"
- onClick={onToggleClick}>
- {translate('more')}
- <i className="icon-dropdown little-spacer-left" />
- </a>
-
- <ul className="dropdown-menu">
- {extensions.map(extension => (
- <li key={extension.key}>
- <Link
- to={`/organizations/${organization.key}/extension/${extension.key}`}
- activeClassName="active">
- {extension.name}
- </Link>
- </li>
- ))}
- </ul>
- </li>
- )}
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ {extensions.map(extension => (
+ <li key={extension.key}>
+ <Link
+ activeClassName="active"
+ to={`/organizations/${organization.key}/extension/${extension.key}`}>
+ {extension.name}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ }
+ tagName="li">
+ <a
+ className={classNames('dropdown-toggle', { active })}
+ href="#"
+ id="organization-navigation-more">
+ {translate('more')}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
index c6bc236126a..df497ccd8fc 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import { sortBy } from 'lodash';
import { Organization } from '../../../app/types';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
@@ -38,20 +37,19 @@ export default function OrganizationNavigationHeader({ organization, organizatio
<header className="navbar-context-header">
<OrganizationAvatar organization={organization} />
{other.length ? (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('organization-switch', 'dropdown', { open })}>
- <a className="dropdown-toggle" href="#" onClick={onToggleClick}>
- {organization.name}
- <DropdownIcon className="little-spacer-left" />
- </a>
- <ul className="dropdown-menu">
- {sortBy(other, org => org.name.toLowerCase()).map(organization => (
- <OrganizationListItem key={organization.key} organization={organization} />
- ))}
- </ul>
- </div>
- )}
+ <Dropdown
+ className="display-inline-block"
+ overlay={
+ <ul className="menu">
+ {sortBy(other, org => org.name.toLowerCase()).map(organization => (
+ <OrganizationListItem key={organization.key} organization={organization} />
+ ))}
+ </ul>
+ }>
+ <a className="spacer-left link-base-color link-no-underline" href="#">
+ {organization.name}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
</Dropdown>
) : (
<span className="spacer-left">{organization.name}</span>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx
index f68fab3ac9f..eb8baeef190 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationAdministration-test.tsx
@@ -33,5 +33,5 @@ it('renders', () => {
}}
/>
);
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
index f6afd014d72..57d43dc4682 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
@@ -52,5 +52,5 @@ it('renders dropdown', () => {
organizations={organizations}
/>
);
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
index c7764bf1c8b..27d6f917f78 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
@@ -1,83 +1,84 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders 1`] = `
-<li
- className="dropdown"
+<Dropdown
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/groups"
+ >
+ user_groups.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/permissions"
+ >
+ permissions.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/permission_templates"
+ >
+ permission_templates
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/projects_management"
+ >
+ projects_management
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/webhooks"
+ >
+ webhooks.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/edit"
+ >
+ edit
+ </Link>
+ </li>
+ </ul>
+ }
+ tagName="li"
>
<a
className="dropdown-toggle"
href="#"
id="organization-navigation-admin"
- onClick={[Function]}
>
layout.settings
<DropdownIcon
className="little-spacer-left"
/>
</a>
- <ul
- className="dropdown-menu"
- >
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/groups"
- >
- user_groups.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/permissions"
- >
- permissions.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/permission_templates"
- >
- permission_templates
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/projects_management"
- >
- projects_management
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/webhooks"
- >
- webhooks.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/edit"
- >
- edit
- </Link>
- </li>
- </ul>
-</li>
+</Dropdown>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
index 949bb39ff04..626ef846617 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
@@ -22,44 +22,43 @@ exports[`renders 1`] = `
`;
exports[`renders dropdown 1`] = `
-<div
- className="organization-switch dropdown"
+<Dropdown
+ className="display-inline-block"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <OrganizationListItem
+ organization={
+ Object {
+ "isAdmin": true,
+ "key": "org1",
+ "name": "org1",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <OrganizationListItem
+ organization={
+ Object {
+ "isAdmin": false,
+ "key": "org2",
+ "name": "org2",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ </ul>
+ }
>
<a
- className="dropdown-toggle"
+ className="spacer-left link-base-color link-no-underline"
href="#"
- onClick={[Function]}
>
Foo
<DropdownIcon
className="little-spacer-left"
/>
</a>
- <ul
- className="dropdown-menu"
- >
- <OrganizationListItem
- key="org1"
- organization={
- Object {
- "isAdmin": true,
- "key": "org1",
- "name": "org1",
- "projectVisibility": "public",
- }
- }
- />
- <OrganizationListItem
- key="org2"
- organization={
- Object {
- "isAdmin": false,
- "key": "org2",
- "name": "org2",
- "projectVisibility": "public",
- }
- }
- />
- </ul>
-</div>
+</Dropdown>
`;
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
index bdc74e4d941..855b3201c0c 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
@@ -22,58 +22,20 @@ import MetaTagsSelector from './MetaTagsSelector';
import { setProjectTags } from '../../../api/components';
import { translate } from '../../../helpers/l10n';
import TagsList from '../../../components/tags/TagsList';
-import { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { Component } from '../../../app/types';
import { Button } from '../../../components/ui/buttons';
+import Dropdown from '../../../components/controls/Dropdown';
+import { PopupPlacement } from '../../../components/ui/popups';
interface Props {
component: Component;
onComponentChange: (changes: {}) => void;
}
-interface State {
- popupOpen: boolean;
- popupPosition: BubblePopupPosition;
-}
-
-export default class MetaTags extends React.PureComponent<Props, State> {
+export default class MetaTags extends React.PureComponent<Props> {
card?: HTMLDivElement | null;
tagsList?: HTMLElement | null;
tagsSelector?: HTMLDivElement | null;
- state: State = { popupOpen: false, popupPosition: { top: 0, right: 0 } };
-
- componentDidMount() {
- if (this.canUpdateTags() && this.tagsList && this.card) {
- const buttonPos = this.tagsList.getBoundingClientRect();
- const cardPos = this.card.getBoundingClientRect();
- this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) });
-
- window.addEventListener('keydown', this.handleKey, false);
- window.addEventListener('click', this.handleOutsideClick, false);
- }
- }
-
- componentWillUnmount() {
- window.removeEventListener('keydown', this.handleKey);
- window.removeEventListener('click', this.handleOutsideClick);
- }
-
- handleKey = (evt: KeyboardEvent) => {
- // Escape key
- if (evt.keyCode === 27) {
- this.setState({ popupOpen: false });
- }
- };
-
- handleOutsideClick = (evt: Event) => {
- if (!this.tagsSelector || !this.tagsSelector.contains(evt.target as Node)) {
- this.setState({ popupOpen: false });
- }
- };
-
- handleClick = () => {
- this.setState(state => ({ popupOpen: !state.popupOpen }));
- };
canUpdateTags = () => {
const { configuration } = this.props.component;
@@ -94,29 +56,29 @@ export default class MetaTags extends React.PureComponent<Props, State> {
render() {
const { key } = this.props.component;
- const { popupOpen, popupPosition } = this.state;
const tags = this.props.component.tags || [];
if (this.canUpdateTags()) {
return (
<div className="big-spacer-top overview-meta-tags" ref={card => (this.card = card)}>
- <Button
- className="button-link"
- innerRef={tagsList => (this.tagsList = tagsList)}
- onClick={this.handleClick}
- stopPropagation={true}>
- <TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} />
- </Button>
- {popupOpen && (
- <div ref={tagsSelector => (this.tagsSelector = tagsSelector)}>
+ <Dropdown
+ closeOnClick={false}
+ closeOnClickOutside={true}
+ overlay={
<MetaTagsSelector
- position={popupPosition}
project={key}
selectedTags={tags}
setProjectTags={this.handleSetProjectTags}
/>
- </div>
- )}
+ }
+ overlayPlacement={PopupPlacement.BottomLeft}>
+ <Button
+ className="button-link"
+ innerRef={tagsList => (this.tagsList = tagsList)}
+ stopPropagation={true}>
+ <TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} />
+ </Button>
+ </Dropdown>
</div>
);
} else {
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
index 783688ebf7c..fcc2952d745 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
@@ -20,11 +20,9 @@
import * as React from 'react';
import { without, difference } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
-import { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { searchProjectTags } from '../../../api/components';
interface Props {
- position: BubblePopupPosition;
project: string;
selectedTags: string[];
setProjectTags: (tags: string[]) => void;
@@ -78,7 +76,6 @@ export default class MetaTagsSelector extends React.PureComponent<Props, State>
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
- position={this.props.position}
selectedTags={this.props.selectedTags}
tags={availableTags}
/>
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx
index 7fa2003a331..b2ed2b1ac1e 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
import MetaTags from '../MetaTags';
const component = {
@@ -61,21 +60,3 @@ it('should render with tags and admin rights', () => {
})
).toMatchSnapshot();
});
-
-it('should open the tag selector on click', () => {
- const wrapper = shallow(
- <MetaTags component={componentWithTags} onComponentChange={jest.fn()} />,
- {
- disableLifecycleMethods: true
- }
- );
- expect(wrapper).toMatchSnapshot();
-
- // open
- click(wrapper.find('Button'));
- expect(wrapper).toMatchSnapshot();
-
- // close
- click(wrapper.find('Button'));
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx
index aefeb8d6fb6..0c4c24c9b1b 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx
@@ -38,26 +38,14 @@ it('searches tags on mount', () => {
(searchProjectTags as jest.Mock).mockImplementation(() =>
Promise.resolve({ tags: ['foo', 'bar'] })
);
- mount(
- <MetaTagsSelector
- position={{ top: 0, right: 0 }}
- project="foo"
- selectedTags={[]}
- setProjectTags={jest.fn()}
- />
- );
+ mount(<MetaTagsSelector project="foo" selectedTags={[]} setProjectTags={jest.fn()} />);
expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' });
});
it('selects and deselects tags', () => {
const setProjectTags = jest.fn();
const wrapper = shallow(
- <MetaTagsSelector
- position={{ top: 0, right: 0 }}
- project="foo"
- selectedTags={['foo', 'bar']}
- setProjectTags={setProjectTags}
- />
+ <MetaTagsSelector project="foo" selectedTags={['foo', 'bar']} setProjectTags={setProjectTags} />
);
const tagSelect: any = wrapper.find('TagsSelector');
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
index d0b063accf7..7eb0fedde2c 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
@@ -1,112 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should open the tag selector on click 1`] = `
-<div
- className="big-spacer-top overview-meta-tags"
->
- <Button
- className="button-link"
- innerRef={[Function]}
- onClick={[Function]}
- stopPropagation={true}
- >
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "foo",
- "bar",
- ]
- }
- />
- </Button>
-</div>
-`;
-
-exports[`should open the tag selector on click 2`] = `
-<div
- className="big-spacer-top overview-meta-tags"
->
- <Button
- className="button-link"
- innerRef={[Function]}
- onClick={[Function]}
- stopPropagation={true}
- >
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "foo",
- "bar",
- ]
- }
- />
- </Button>
- <div>
- <MetaTagsSelector
- position={
- Object {
- "right": 0,
- "top": 0,
- }
- }
- project="my-second-project"
- selectedTags={
- Array [
- "foo",
- "bar",
- ]
- }
- setProjectTags={[Function]}
- />
- </div>
-</div>
-`;
-
-exports[`should open the tag selector on click 3`] = `
-<div
- className="big-spacer-top overview-meta-tags"
->
- <Button
- className="button-link"
- innerRef={[Function]}
- onClick={[Function]}
- stopPropagation={true}
- >
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "foo",
- "bar",
- ]
- }
- />
- </Button>
-</div>
-`;
-
exports[`should render with tags and admin rights 1`] = `
<div
className="big-spacer-top overview-meta-tags"
>
- <Button
- className="button-link"
- innerRef={[Function]}
- onClick={[Function]}
- stopPropagation={true}
+ <Dropdown
+ closeOnClick={false}
+ closeOnClickOutside={true}
+ overlay={
+ <MetaTagsSelector
+ project="my-second-project"
+ selectedTags={
+ Array [
+ "foo",
+ "bar",
+ ]
+ }
+ setProjectTags={[Function]}
+ />
+ }
+ overlayPlacement="bottom-left"
>
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "foo",
- "bar",
- ]
- }
- />
- </Button>
+ <Button
+ className="button-link"
+ innerRef={[Function]}
+ stopPropagation={true}
+ >
+ <TagsList
+ allowUpdate={true}
+ tags={
+ Array [
+ "foo",
+ "bar",
+ ]
+ }
+ />
+ </Button>
+ </Dropdown>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css
index 0d0dc6dbee9..99b68c86254 100644
--- a/server/sonar-web/src/main/js/apps/overview/styles.css
+++ b/server/sonar-web/src/main/js/apps/overview/styles.css
@@ -389,15 +389,11 @@
border: none;
}
-.overview-analysis-graph .bubble-popup {
+.overview-analysis-graph-popup {
opacity: 0.8;
padding: 0;
}
-.overview-analysis-graph .bubble-popup-arrow {
- top: 7px;
-}
-
.overview-analysis-graph-tooltip {
padding: 4px;
pointer-events: none;
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
index 121c008ce9a..7ef344a3b23 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { difference } from 'lodash';
+import DeleteForm from './DeleteForm';
import Form from './Form';
import {
setDefaultPermissionTemplate,
@@ -28,11 +29,10 @@ import {
} from '../../../api/permissions';
import { PermissionTemplate } from '../../../app/types';
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
import QualifierIcon from '../../../components/shared/QualifierIcon';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
-interface Props {
+export interface Props {
fromDetails?: boolean;
organization?: { isDefault?: boolean; key: string };
permissionTemplate: PermissionTemplate;
@@ -41,6 +41,7 @@ interface Props {
}
interface State {
+ deleteForm: boolean;
updateModal: boolean;
}
@@ -51,7 +52,7 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
router: PropTypes.object
};
- state: State = { updateModal: false };
+ state: State = { deleteForm: false, updateModal: false };
componentDidMount() {
this.mounted = true;
@@ -81,8 +82,18 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
);
};
- handleDelete = (templateId: string) => {
- return deletePermissionTemplate({ templateId }).then(() => {
+ handleDeleteClick = () => {
+ this.setState({ deleteForm: true });
+ };
+
+ handleCloseDeleteForm = () => {
+ if (this.mounted) {
+ this.setState({ deleteForm: false });
+ }
+ };
+
+ handleDeleteSubmit = () => {
+ return deletePermissionTemplate({ templateId: this.props.permissionTemplate.id }).then(() => {
const pathname = this.props.organization
? `/organizations/${this.props.organization.key}/permission_templates`
: '/permission_templates';
@@ -159,18 +170,30 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
: '/permission_templates';
return (
- <ActionsDropdown>
- {this.renderSetDefaultsControl()}
-
- {!this.props.fromDetails && (
- <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}>
- {translate('edit_permissions')}
+ <>
+ <ActionsDropdown>
+ {this.renderSetDefaultsControl()}
+
+ {!this.props.fromDetails && (
+ <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}>
+ {translate('edit_permissions')}
+ </ActionsDropdownItem>
+ )}
+
+ <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}>
+ {translate('update_details')}
</ActionsDropdownItem>
- )}
- <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}>
- {translate('update_details')}
- </ActionsDropdownItem>
+ {t.defaultFor.length === 0 && (
+ <ActionsDropdownItem
+ className="js-delete"
+ destructive={true}
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ )}
+ </ActionsDropdown>
+
{this.state.updateModal && (
<Form
confirmButtonText={translate('update_verb')}
@@ -181,25 +204,14 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
/>
)}
- {t.defaultFor.length === 0 && (
- <ConfirmButton
- confirmButtonText={translate('delete')}
- confirmData={t.id}
- isDestructive={true}
- modalBody={translateWithParameters(
- 'permission_template.do_you_want_to_delete_template_xxx',
- t.name
- )}
- modalHeader={translate('permission_template.delete_confirm_title')}
- onConfirm={this.handleDelete}>
- {({ onClick }) => (
- <ActionsDropdownItem className="js-delete" destructive={true} onClick={onClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
- </ConfirmButton>
+ {this.state.deleteForm && (
+ <DeleteForm
+ onClose={this.handleCloseDeleteForm}
+ onSubmit={this.handleDeleteSubmit}
+ permissionTemplate={t}
+ />
)}
- </ActionsDropdown>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx
new file mode 100644
index 00000000000..b89b76e1c31
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { PermissionTemplate } from '../../../app/types';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ onClose: () => void;
+ onSubmit: () => Promise<void>;
+ permissionTemplate: PermissionTemplate;
+}
+
+export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }: Props) {
+ const header = translate('permission_template.delete_confirm_title');
+
+ return (
+ <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters(
+ 'permission_template.do_you_want_to_delete_template_xxx',
+ t.name
+ )}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
index b4c48de8bf8..a3ea017810c 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
@@ -17,23 +17,24 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import * as React from 'react';
import { shallow } from 'enzyme';
-import React from 'react';
-import ActionsCell from '../ActionsCell';
+import ActionsCell, { Props } from '../ActionsCell';
const SAMPLE = {
+ createdAt: '2018-01-01',
id: 'id',
name: 'name',
permissions: [],
defaultFor: []
};
-function renderActionsCell(props) {
+function renderActionsCell(props?: Partial<Props>) {
return shallow(
<ActionsCell
permissionTemplate={SAMPLE}
- topQualifiers={['TRK', 'VW']}
refresh={() => true}
+ topQualifiers={['TRK', 'VW']}
{...props}
/>
);
@@ -53,7 +54,7 @@ it('should not set default', () => {
});
it('should display all qualifiers for default organization', () => {
- const organization = { isDefault: true };
+ const organization = { isDefault: true, key: 'org' };
const setDefault = renderActionsCell({ organization }).find('.js-set-default');
expect(setDefault.length).toBe(2);
expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
@@ -61,7 +62,7 @@ it('should display all qualifiers for default organization', () => {
});
it('should display only projects for custom organization', () => {
- const organization = { isDefault: false };
+ const organization = { isDefault: false, key: 'org' };
const setDefault = renderActionsCell({ organization }).find('.js-set-default');
expect(setDefault.length).toBe(1);
expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
index cf17cc3b037..ed7b41bb49b 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
@@ -19,15 +19,14 @@
*/
// @flow
import React from 'react';
-import classNames from 'classnames';
import GraphsTooltipsContent from './GraphsTooltipsContent';
import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents';
import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues';
import { DEFAULT_GRAPH } from '../utils';
-import BubblePopup from '../../../components/common/BubblePopup';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import { Popup, PopupPlacement } from '../../../components/ui/popups';
/*:: import type { Event, MeasureHistory } from '../types'; */
/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
@@ -88,17 +87,18 @@ export default class GraphsTooltips extends React.PureComponent {
const { events, measuresHistory, tooltipIdx } = this.props;
const top = 30;
let left = this.props.tooltipPos + 60;
- let customClass;
+ let placement = PopupPlacement.RightTop;
if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) {
left -= TOOLTIP_WIDTH;
- customClass = 'bubble-popup-right';
+ placement = PopupPlacement.LeftTop;
}
const tooltipContent = this.renderContent().filter(Boolean);
const addSeparator = tooltipContent.length > 0;
return (
- <BubblePopup
- customClass={classNames(customClass, 'disabled-pointer-events')}
- position={{ top, left, width: TOOLTIP_WIDTH }}>
+ <Popup
+ className="disabled-pointer-events"
+ placement={placement}
+ style={{ top, left, width: TOOLTIP_WIDTH }}>
<div className="project-activity-graph-tooltip">
<div className="project-activity-graph-tooltip-title spacer-bottom">
<DateTimeFormatter date={this.props.selectedDate} />
@@ -125,7 +125,7 @@ export default class GraphsTooltips extends React.PureComponent {
)}
</table>
</div>
- </BubblePopup>
+ </Popup>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
index 5324153a4e0..d34e8de5448 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
@@ -25,7 +25,8 @@ import AddEventForm from './forms/AddEventForm';
import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter';
import ActionsDropdown, {
- ActionsDropdownDivider
+ ActionsDropdownDivider,
+ ActionsDropdownItem
} from '../../../components/controls/ActionsDropdown';
import { translate } from '../../../helpers/l10n';
/*:: import type { Analysis } from '../types'; */
@@ -45,15 +46,61 @@ type Props = {
selected: boolean,
updateSelectedDate: Date => void
};
+
+type State = {
+ addEventForm: bool,
+ addVersionForm: bool,
+ removeAnalysisForm: bool
+}
*/
export default class ProjectActivityAnalysis extends React.PureComponent {
+ mounted /*: boolean */ = false;
/*:: props: Props; */
+ state /*: State */ = { addEventForm: false, addVersionForm: false, removeAnalysisForm: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
handleClick = () => this.props.updateSelectedDate(this.props.analysis.date);
stopPropagation = (e /*: Event */) => e.stopPropagation();
+ handleRemoveAnalysisClick = () => {
+ this.setState({ removeAnalysisForm: true });
+ };
+
+ closeRemoveAnalysisForm = () => {
+ if (this.mounted) {
+ this.setState({ removeAnalysisForm: false });
+ }
+ };
+
+ handleAddEventClick = () => {
+ this.setState({ addEventForm: true });
+ };
+
+ closeAddEventForm = () => {
+ if (this.mounted) {
+ this.setState({ addEventForm: false });
+ }
+ };
+
+ handleAddVersionClick = () => {
+ this.setState({ addVersionForm: true });
+ };
+
+ closeAddVersionForm = () => {
+ if (this.mounted) {
+ this.setState({ addVersionForm: false });
+ }
+ };
+
render() {
const { analysis, isFirst, canAdmin } = this.props;
const { date, events } = analysis;
@@ -71,7 +118,6 @@ export default class ProjectActivityAnalysis extends React.PureComponent {
})}
data-date={date.valueOf()}
onClick={this.handleClick}
- role="listitem"
tabIndex="0">
<div className="project-activity-time spacer-right">
<TimeTooltipFormatter className="text-middle" date={date} />
@@ -80,29 +126,53 @@ export default class ProjectActivityAnalysis extends React.PureComponent {
{(canAddVersion || canAddEvent || canDeleteAnalyses) && (
<div className="project-activity-analysis-actions big-spacer-right">
- <ActionsDropdown menuPosition="left" small={true} toggleClassName="js-analysis-actions">
+ <ActionsDropdown small={true} toggleClassName="js-analysis-actions">
{canAddVersion && (
- <AddEventForm
- addEvent={this.props.addVersion}
- analysis={analysis}
- addEventButtonText="project_activity.add_version"
- />
+ <ActionsDropdownItem className="js-add-event" onClick={this.handleAddVersionClick}>
+ {translate('project_activity.add_version')}
+ </ActionsDropdownItem>
)}
{canAddEvent && (
- <AddEventForm
- addEvent={this.props.addCustomEvent}
- analysis={analysis}
- addEventButtonText="project_activity.add_custom_event"
- />
+ <ActionsDropdownItem className="js-add-event" onClick={this.handleAddEventClick}>
+ {translate('project_activity.add_custom_event')}
+ </ActionsDropdownItem>
)}
{(canAddVersion || canAddEvent) && canDeleteAnalyses && <ActionsDropdownDivider />}
{canDeleteAnalyses && (
- <RemoveAnalysisForm
- analysis={analysis}
- deleteAnalysis={this.props.deleteAnalysis}
- />
+ <ActionsDropdownItem
+ className="js-delete-analysis"
+ destructive={true}
+ onClick={this.handleRemoveAnalysisClick}>
+ {translate('project_activity.delete_analysis')}
+ </ActionsDropdownItem>
)}
</ActionsDropdown>
+
+ {this.state.addVersionForm && (
+ <AddEventForm
+ addEvent={this.props.addVersion}
+ addEventButtonText="project_activity.add_version"
+ analysis={analysis}
+ onClose={this.closeAddVersionForm}
+ />
+ )}
+
+ {this.state.addEventForm && (
+ <AddEventForm
+ addEvent={this.props.addCustomEvent}
+ addEventButtonText="project_activity.add_custom_event"
+ analysis={analysis}
+ onClose={this.closeAddEventForm}
+ />
+ )}
+
+ {this.state.removeAnalysisForm && (
+ <RemoveAnalysisForm
+ analysis={analysis}
+ deleteAnalysis={this.props.deleteAnalysis}
+ onClose={this.closeRemoveAnalysisForm}
+ />
+ )}
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
index c2723826cfe..3089427f9ac 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
@@ -1,9 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not add separators if not needed 1`] = `
-<BubblePopup
- customClass="bubble-popup-right disabled-pointer-events"
- position={
+<Popup
+ className="disabled-pointer-events"
+ placement="left-top"
+ style={
Object {
"left": 476,
"top": 30,
@@ -32,13 +33,14 @@ exports[`should not add separators if not needed 1`] = `
/>
</table>
</div>
-</BubblePopup>
+</Popup>
`;
exports[`should render correctly for issues graphs 1`] = `
-<BubblePopup
- customClass="bubble-popup-right disabled-pointer-events"
- position={
+<Popup
+ className="disabled-pointer-events"
+ placement="left-top"
+ style={
Object {
"left": 476,
"top": 30,
@@ -90,13 +92,14 @@ exports[`should render correctly for issues graphs 1`] = `
</tbody>
</table>
</div>
-</BubblePopup>
+</Popup>
`;
exports[`should render correctly for random graphs 1`] = `
-<BubblePopup
- customClass="bubble-popup-right disabled-pointer-events"
- position={
+<Popup
+ className="disabled-pointer-events"
+ placement="left-top"
+ style={
Object {
"left": 476,
"top": 30,
@@ -142,5 +145,5 @@ exports[`should render correctly for random graphs 1`] = `
</tbody>
</table>
</div>
-</BubblePopup>
+</Popup>
`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
index b47c72d926b..ded7c2b733c 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
@@ -20,7 +20,6 @@
// @flow
import React from 'react';
import Modal from '../../../../components/controls/Modal';
-import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown';
import { translate } from '../../../../helpers/l10n';
/*:: import type { Analysis } from '../../types'; */
@@ -28,13 +27,13 @@ import { translate } from '../../../../helpers/l10n';
type Props = {
addEvent: (analysis: string, name: string, category?: string) => Promise<*>,
analysis: Analysis,
- addEventButtonText: string
+ addEventButtonText: string,
+ onClose: () => void;
};
*/
/*::
type State = {
- open: boolean,
processing: boolean,
name: string
};
@@ -44,7 +43,6 @@ export default class AddEventForm extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
- open: false,
processing: false,
name: ''
};
@@ -57,16 +55,6 @@ export default class AddEventForm extends React.PureComponent {
this.mounted = false;
}
- openForm = () => {
- this.setState({ open: true });
- };
-
- closeForm = () => {
- if (this.mounted) {
- this.setState({ open: false, name: '' });
- }
- };
-
changeInput = (e /*: Object */) => {
if (this.mounted) {
this.setState({ name: e.target.value });
@@ -79,24 +67,18 @@ export default class AddEventForm extends React.PureComponent {
}
};
- stopProcessingAndClose = () => {
- if (this.mounted) {
- this.setState({ open: false, processing: false, name: '' });
- }
- };
-
handleSubmit = (e /*: Object */) => {
e.preventDefault();
this.setState({ processing: true });
this.props
.addEvent(this.props.analysis.key, this.state.name)
- .then(this.stopProcessingAndClose, this.stopProcessing);
+ .then(this.props.onClose, this.stopProcessing);
};
- renderModal() {
+ render() {
const header = translate(this.props.addEventButtonText);
return (
- <Modal key="add-event-modal" contentLabel={header} onRequestClose={this.closeForm}>
+ <Modal contentLabel={header} key="add-event-modal" onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
@@ -106,11 +88,11 @@ export default class AddEventForm extends React.PureComponent {
<div className="modal-field">
<label>{translate('name')}</label>
<input
- value={this.state.name}
autoFocus={true}
disabled={this.state.processing}
- type="text"
onChange={this.changeInput}
+ type="text"
+ value={this.state.name}
/>
</div>
</div>
@@ -121,7 +103,7 @@ export default class AddEventForm extends React.PureComponent {
) : (
<div>
<button type="submit">{translate('save')}</button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
{translate('cancel')}
</button>
</div>
@@ -131,16 +113,4 @@ export default class AddEventForm extends React.PureComponent {
</Modal>
);
}
-
- render() {
- const linkComponent = (
- <ActionsDropdownItem className="js-add-event" onClick={this.openForm}>
- {translate(this.props.addEventButtonText)}
- </ActionsDropdownItem>
- );
- if (this.state.open) {
- return [linkComponent, this.renderModal()];
- }
- return linkComponent;
- }
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
index 78b47814b1b..3bf12702367 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
@@ -22,7 +22,8 @@ import React from 'react';
import { find, sortBy } from 'lodash';
import AddGraphMetricPopup from './AddGraphMetricPopup';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
-import BubblePopupHelper from '../../../../components/common/BubblePopupHelper';
+import Dropdown from '../../../../components/controls/Dropdown';
+import { Button } from '../../../../components/ui/buttons';
import { isDiffMetric } from '../../../../helpers/measures';
import { getLocalizedMetricName, translate } from '../../../../helpers/l10n';
/*:: import type { Metric } from '../../types'; */
@@ -40,7 +41,6 @@ type Props = {
/*::
type State = {
- open: boolean,
query: string,
};
*/
@@ -48,7 +48,6 @@ type State = {
export default class AddGraphMetric extends React.PureComponent {
/*:: props: Props; */
state /*: State */ = {
- open: false,
metrics: [],
query: '',
selectedMetrics: []
@@ -107,12 +106,6 @@ export default class AddGraphMetric extends React.PureComponent {
return metric === undefined ? key : getLocalizedMetricName(metric);
};
- toggleForm = () => {
- this.setState(state => {
- return { open: !state.open };
- });
- };
-
onSearch = (query /*: string */) => {
this.setState({ query });
return Promise.resolve();
@@ -138,10 +131,6 @@ export default class AddGraphMetric extends React.PureComponent {
});
};
- togglePopup = (open /*: boolean*/) => {
- this.setState({ open });
- };
-
render() {
const { query } = this.state;
const filteredMetrics = this.filterMetricsElements(this.props, query);
@@ -151,34 +140,27 @@ export default class AddGraphMetric extends React.PureComponent {
query
);
return (
- <div className="display-inline-block">
- <BubblePopupHelper
- isOpen={this.state.open}
- offset={{ horizontal: 16, vertical: 0 }}
- popup={
- <AddGraphMetricPopup
- elements={filteredMetrics}
- filterSelected={this.filterSelected}
- metricsTypeFilter={this.props.metricsTypeFilter}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- renderLabel={element => this.getLocalizedMetricNameFromKey(element)}
- selectedElements={selectedMetrics}
- />
- }
- position="bottomright"
- togglePopup={this.togglePopup}>
- <button className="spacer-left" onClick={this.toggleForm} type="button">
- <span>
- <span className="text-ellipsis spacer-right">
- {translate('project_activity.graphs.custom.add')}
- </span>
- <DropdownIcon className="vertical-text-top" />
- </span>
- </button>
- </BubblePopupHelper>
- </div>
+ <Dropdown
+ className="display-inline-block"
+ overlay={
+ <AddGraphMetricPopup
+ elements={filteredMetrics}
+ filterSelected={this.filterSelected}
+ metricsTypeFilter={this.props.metricsTypeFilter}
+ onSearch={this.onSearch}
+ onSelect={this.onSelect}
+ onUnselect={this.onUnselect}
+ renderLabel={element => this.getLocalizedMetricNameFromKey(element)}
+ selectedElements={selectedMetrics}
+ />
+ }>
+ <Button className="spacer-left">
+ <span className="text-ellipsis text-middle">
+ {translate('project_activity.graphs.custom.add')}
+ </span>
+ <DropdownIcon className="text-top little-spacer-left" />
+ </Button>
+ </Dropdown>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
index 81864bf8920..d4bf8bbd2e7 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import BubblePopup from '../../../../components/common/BubblePopup';
import MultiSelect from '../../../../components/common/MultiSelect';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
@@ -58,9 +57,7 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr
}
return (
- <BubblePopup
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
- position={props.popupPosition}>
+ <div className="menu abs-width-300">
<MultiSelect
allowNewElements={false}
allowSelection={props.selectedElements.length < 6}
@@ -74,6 +71,6 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr
renderLabel={props.renderLabel}
selectedElements={props.selectedElements}
/>
- </BubblePopup>
+ </div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
index 47125ed09fe..e813985139a 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
@@ -20,20 +20,19 @@
// @flow
import React from 'react';
import Modal from '../../../../components/controls/Modal';
-import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown';
import { translate } from '../../../../helpers/l10n';
/*:: import type { Analysis } from '../../types'; */
/*::
type Props = {
analysis: Analysis,
- deleteAnalysis: (analysis: string) => Promise<*>
+ deleteAnalysis: (analysis: string) => Promise<*>,
+ onClose: () => void;
};
*/
/*::
type State = {
- open: boolean,
processing: boolean
};
*/
@@ -42,7 +41,6 @@ export default class RemoveAnalysisForm extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
- open: false,
processing: false
};
@@ -54,40 +52,24 @@ export default class RemoveAnalysisForm extends React.PureComponent {
this.mounted = false;
}
- openForm = () => {
- this.setState({ open: true });
- };
-
- closeForm = () => {
- if (this.mounted) {
- this.setState({ open: false });
- }
- };
-
stopProcessing = () => {
if (this.mounted) {
this.setState({ processing: false });
}
};
- stopProcessingAndClose = () => {
- if (this.mounted) {
- this.setState({ open: false, processing: false });
- }
- };
-
handleSubmit = (e /*: Event */) => {
e.preventDefault();
this.setState({ processing: true });
this.props
.deleteAnalysis(this.props.analysis.key)
- .then(this.stopProcessingAndClose, this.stopProcessing);
+ .then(this.props.onClose, this.stopProcessing);
};
- renderModal() {
+ render() {
const header = translate('project_activity.delete_analysis');
return (
- <Modal key="delete-analysis-modal" contentLabel={header} onRequestClose={this.closeForm}>
+ <Modal contentLabel={header} key="delete-analysis-modal" onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
@@ -100,10 +82,10 @@ export default class RemoveAnalysisForm extends React.PureComponent {
<i className="spinner" />
) : (
<div>
- <button type="submit" className="button-red" autoFocus={true}>
+ <button autoFocus={true} className="button-red" type="submit">
{translate('delete')}
</button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
{translate('cancel')}
</button>
</div>
@@ -113,19 +95,4 @@ export default class RemoveAnalysisForm extends React.PureComponent {
</Modal>
);
}
-
- render() {
- const linkComponent = (
- <ActionsDropdownItem
- className="js-delete-analysis"
- destructive={true}
- onClick={this.openForm}>
- {translate('project_activity.delete_analysis')}
- </ActionsDropdownItem>
- );
- if (this.state.open) {
- return [linkComponent, this.renderModal()];
- }
- return linkComponent;
- }
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
index 6298f16e0a3..f0ec4ed6f3c 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
@@ -153,15 +153,17 @@ export default class App extends React.PureComponent<Props, State> {
return (
<React.Fragment key={getBranchLikeKey(branchLike)}>
{showOrphanHeader && (
- <li className="dropdown-header">
- <div className="display-inline-block text-middle">
- {translate('branches.orphan_branches')}
- </div>
- <HelpTooltip
- className="spacer-left"
- overlay={translate('branches.orphan_branches.tooltip')}
- />
- </li>
+ <tr>
+ <td colSpan={4}>
+ <div className="display-inline-block text-middle">
+ {translate('branches.orphan_branches')}
+ </div>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={translate('branches.orphan_branches.tooltip')}
+ />
+ </td>
+ </tr>
)}
<BranchRow
branchLike={branchLike}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
index 5c8f4b20cf0..2770b226fdc 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -147,19 +147,21 @@ exports[`renders sorted list of branches 1`] = `
<React.Fragment
key="branch-feature"
>
- <li
- className="dropdown-header"
- >
- <div
- className="display-inline-block text-middle"
+ <tr>
+ <td
+ colSpan={4}
>
- branches.orphan_branches
- </div>
- <HelpTooltip
- className="spacer-left"
- overlay="branches.orphan_branches.tooltip"
- />
- </li>
+ <div
+ className="display-inline-block text-middle"
+ >
+ branches.orphan_branches
+ </div>
+ <HelpTooltip
+ className="spacer-left"
+ overlay="branches.orphan_branches.tooltip"
+ />
+ </td>
+ </tr>
<BranchRow
branchLike={
Object {
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
index daa2f082aae..935ff76d795 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
-import * as classNames from 'classnames';
import { connect } from 'react-redux';
import * as PropTypes from 'prop-types';
import { sortBy } from 'lodash';
@@ -61,23 +60,19 @@ export class NoFavoriteProjects extends React.PureComponent<Props> {
<a className="button" href="#" onClick={this.onAnalyzeProjectClick}>
{translate('my_account.analyze_new_project')}
</a>
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div
- className={classNames('display-inline-block', 'big-spacer-left', 'dropdown', {
- open
- })}>
- <a className="button" href="#" onClick={onToggleClick}>
- {translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
- <DropdownIcon className="little-spacer-left" />
- </a>
- <ul className="dropdown-menu">
- {sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
- <OrganizationListItem key={organization.key} organization={organization} />
- ))}
- </ul>
- </div>
- )}
+ <Dropdown
+ className="display-inline-block big-spacer-left"
+ overlay={
+ <ul className="menu">
+ {sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+ <OrganizationListItem key={organization.key} organization={organization} />
+ ))}
+ </ul>
+ }>
+ <a className="button" href="#">
+ {translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
</Dropdown>
<Link className="button big-spacer-left" to="/explore/projects">
{translate('projects.no_favorite_projects.favorite_public_projects')}
@@ -90,7 +85,7 @@ export class NoFavoriteProjects extends React.PureComponent<Props> {
{translate('projects.no_favorite_projects.engagement')}
</p>
<p className="big-spacer-top">
- <Link to="/projects/all" className="button">
+ <Link className="button" to="/projects/all">
{translate('projects.explore_projects')}
</Link>
</p>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
index a7dede57348..8cf6f203496 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
@@ -52,7 +52,45 @@ exports[`renders for SonarCloud 1`] = `
>
my_account.analyze_new_project
</a>
- <Dropdown />
+ <Dropdown
+ className="display-inline-block big-spacer-left"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <OrganizationListItem
+ organization={
+ Object {
+ "isAdmin": true,
+ "key": "org1",
+ "name": "org1",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <OrganizationListItem
+ organization={
+ Object {
+ "isAdmin": false,
+ "key": "org2",
+ "name": "org2",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ </ul>
+ }
+ >
+ <a
+ className="button"
+ href="#"
+ >
+ projects.no_favorite_projects.favorite_projects_from_orgs
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </a>
+ </Dropdown>
<Link
className="button big-spacer-left"
onlyActiveOnIndex={false}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
index 4a66fe59a8c..438db5c8a77 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
@@ -106,24 +106,28 @@ export default class ProjectRowActions extends React.PureComponent<Props, State>
const { hasAccess } = this.state;
return (
- <ActionsDropdown onOpen={this.handleDropdownOpen}>
- {hasAccess === true && (
- <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}>
- {translate('edit_permissions')}
- </ActionsDropdownItem>
- )}
+ <>
+ <ActionsDropdown onOpen={this.handleDropdownOpen}>
+ {hasAccess === true && (
+ <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}>
+ {translate('edit_permissions')}
+ </ActionsDropdownItem>
+ )}
+
+ {hasAccess === false && (
+ <ActionsDropdownItem
+ className="js-restore-access"
+ onClick={this.handleRestoreAccessClick}>
+ {translate('global_permissions.restore_access')}
+ </ActionsDropdownItem>
+ )}
- {hasAccess === false && (
<ActionsDropdownItem
- className="js-restore-access"
- onClick={this.handleRestoreAccessClick}>
- {translate('global_permissions.restore_access')}
+ className="js-apply-template"
+ onClick={this.handleApplyTemplateClick}>
+ {translate('projects_role.apply_template')}
</ActionsDropdownItem>
- )}
-
- <ActionsDropdownItem className="js-apply-template" onClick={this.handleApplyTemplateClick}>
- {translate('projects_role.apply_template')}
- </ActionsDropdownItem>
+ </ActionsDropdown>
{this.state.restoreAccessModal && (
<RestoreAccessModal
@@ -141,7 +145,7 @@ export default class ProjectRowActions extends React.PureComponent<Props, State>
project={this.props.project}
/>
)}
- </ActionsDropdown>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
index b30e553d91d..df159be98da 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
@@ -44,7 +44,7 @@ it('restores access', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
- wrapper.prop<Function>('onOpen')();
+ wrapper.find('ActionsDropdown').prop<Function>('onOpen')();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
index 61ea2fee3de..cb6c9944857 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
@@ -18,53 +18,59 @@ exports[`applies permission template 1`] = `
`;
exports[`restores access 1`] = `
-<ActionsDropdown
- onOpen={[Function]}
->
- <ActionsDropdownItem
- className="js-apply-template"
- onClick={[Function]}
+<React.Fragment>
+ <ActionsDropdown
+ onOpen={[Function]}
>
- projects_role.apply_template
- </ActionsDropdownItem>
-</ActionsDropdown>
+ <ActionsDropdownItem
+ className="js-apply-template"
+ onClick={[Function]}
+ >
+ projects_role.apply_template
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+</React.Fragment>
`;
exports[`restores access 2`] = `
-<ActionsDropdown
- onOpen={[Function]}
->
- <ActionsDropdownItem
- className="js-restore-access"
- onClick={[Function]}
+<React.Fragment>
+ <ActionsDropdown
+ onOpen={[Function]}
>
- global_permissions.restore_access
- </ActionsDropdownItem>
- <ActionsDropdownItem
- className="js-apply-template"
- onClick={[Function]}
- >
- projects_role.apply_template
- </ActionsDropdownItem>
-</ActionsDropdown>
+ <ActionsDropdownItem
+ className="js-restore-access"
+ onClick={[Function]}
+ >
+ global_permissions.restore_access
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-apply-template"
+ onClick={[Function]}
+ >
+ projects_role.apply_template
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+</React.Fragment>
`;
exports[`restores access 3`] = `
-<ActionsDropdown
- onOpen={[Function]}
->
- <ActionsDropdownItem
- className="js-restore-access"
- onClick={[Function]}
- >
- global_permissions.restore_access
- </ActionsDropdownItem>
- <ActionsDropdownItem
- className="js-apply-template"
- onClick={[Function]}
+<React.Fragment>
+ <ActionsDropdown
+ onOpen={[Function]}
>
- projects_role.apply_template
- </ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-restore-access"
+ onClick={[Function]}
+ >
+ global_permissions.restore_access
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-apply-template"
+ onClick={[Function]}
+ >
+ projects_role.apply_template
+ </ActionsDropdownItem>
+ </ActionsDropdown>
<RestoreAccessModal
currentUser={
Object {
@@ -84,5 +90,5 @@ exports[`restores access 3`] = `
}
}
/>
-</ActionsDropdown>
+</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
index 018d35320f7..17dea8322e9 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
@@ -138,58 +138,60 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
);
return (
- <ActionsDropdown className={this.props.className}>
- {actions.edit && (
- <ActionsDropdownItem to={activateMoreUrl} id="quality-profile-activate-more-rules">
- {translate('quality_profiles.activate_more_rules')}
- </ActionsDropdownItem>
- )}
-
- {!profile.isBuiltIn && (
- <ActionsDropdownItem
- download={`${profile.key}.xml`}
- id="quality-profile-backup"
- to={backupUrl}>
- {translate('backup_verb')}
- </ActionsDropdownItem>
- )}
-
- <ActionsDropdownItem
- id="quality-profile-compare"
- to={getProfileComparePath(profile.name, profile.language, this.props.organization)}>
- {translate('compare')}
- </ActionsDropdownItem>
-
- {actions.copy && (
- <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}>
- {translate('copy')}
- </ActionsDropdownItem>
- )}
+ <>
+ <ActionsDropdown className={this.props.className}>
+ {actions.edit && (
+ <ActionsDropdownItem id="quality-profile-activate-more-rules" to={activateMoreUrl}>
+ {translate('quality_profiles.activate_more_rules')}
+ </ActionsDropdownItem>
+ )}
+
+ {!profile.isBuiltIn && (
+ <ActionsDropdownItem
+ download={`${profile.key}.xml`}
+ id="quality-profile-backup"
+ to={backupUrl}>
+ {translate('backup_verb')}
+ </ActionsDropdownItem>
+ )}
- {actions.edit && (
- <ActionsDropdownItem id="quality-profile-rename" onClick={this.handleRenameClick}>
- {translate('rename')}
- </ActionsDropdownItem>
- )}
-
- {actions.setAsDefault && (
<ActionsDropdownItem
- id="quality-profile-set-as-default"
- onClick={this.handleSetDefaultClick}>
- {translate('set_as_default')}
+ id="quality-profile-compare"
+ to={getProfileComparePath(profile.name, profile.language, this.props.organization)}>
+ {translate('compare')}
</ActionsDropdownItem>
- )}
- {actions.delete && <ActionsDropdownDivider />}
-
- {actions.delete && (
- <ActionsDropdownItem
- destructive={true}
- id="quality-profile-delete"
- onClick={this.handleDeleteClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
+ {actions.copy && (
+ <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}>
+ {translate('copy')}
+ </ActionsDropdownItem>
+ )}
+
+ {actions.edit && (
+ <ActionsDropdownItem id="quality-profile-rename" onClick={this.handleRenameClick}>
+ {translate('rename')}
+ </ActionsDropdownItem>
+ )}
+
+ {actions.setAsDefault && (
+ <ActionsDropdownItem
+ id="quality-profile-set-as-default"
+ onClick={this.handleSetDefaultClick}>
+ {translate('set_as_default')}
+ </ActionsDropdownItem>
+ )}
+
+ {actions.delete && <ActionsDropdownDivider />}
+
+ {actions.delete && (
+ <ActionsDropdownItem
+ destructive={true}
+ id="quality-profile-delete"
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ )}
+ </ActionsDropdown>
{this.state.copyFormOpen && (
<CopyProfileForm
@@ -217,7 +219,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
profile={profile}
/>
)}
- </ActionsDropdown>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
index 01f38890790..86a903e0bef 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
@@ -1,139 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders with all permissions 1`] = `
-<ActionsDropdown>
- <ActionsDropdownItem
- id="quality-profile-activate-more-rules"
- to={
- Object {
- "pathname": "/organizations/org/rules",
- "query": Object {
- "activation": "false",
- "qprofile": "foo",
- },
+<React.Fragment>
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ id="quality-profile-activate-more-rules"
+ to={
+ Object {
+ "pathname": "/organizations/org/rules",
+ "query": Object {
+ "activation": "false",
+ "qprofile": "foo",
+ },
+ }
}
- }
- >
- quality_profiles.activate_more_rules
- </ActionsDropdownItem>
- <ActionsDropdownItem
- download="foo.xml"
- id="quality-profile-backup"
- to="/api/qualityprofiles/backup?profileKey=foo"
- >
- backup_verb
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-compare"
- to={
- Object {
- "pathname": "/organizations/org/quality_profiles/compare",
- "query": Object {
- "language": "java",
- "name": "Foo",
- },
+ >
+ quality_profiles.activate_more_rules
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ download="foo.xml"
+ id="quality-profile-backup"
+ to="/api/qualityprofiles/backup?profileKey=foo"
+ >
+ backup_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-compare"
+ to={
+ Object {
+ "pathname": "/organizations/org/quality_profiles/compare",
+ "query": Object {
+ "language": "java",
+ "name": "Foo",
+ },
+ }
}
- }
- >
- compare
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-copy"
- onClick={[Function]}
- >
- copy
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-rename"
- onClick={[Function]}
- >
- rename
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-set-as-default"
- onClick={[Function]}
- >
- set_as_default
- </ActionsDropdownItem>
- <ActionsDropdownDivider />
- <ActionsDropdownItem
- destructive={true}
- id="quality-profile-delete"
- onClick={[Function]}
- >
- delete
- </ActionsDropdownItem>
-</ActionsDropdown>
+ >
+ compare
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-copy"
+ onClick={[Function]}
+ >
+ copy
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-rename"
+ onClick={[Function]}
+ >
+ rename
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-set-as-default"
+ onClick={[Function]}
+ >
+ set_as_default
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ destructive={true}
+ id="quality-profile-delete"
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+</React.Fragment>
`;
exports[`renders with no permissions 1`] = `
-<ActionsDropdown>
- <ActionsDropdownItem
- download="foo.xml"
- id="quality-profile-backup"
- to="/api/qualityprofiles/backup?profileKey=foo"
- >
- backup_verb
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-compare"
- to={
- Object {
- "pathname": "/organizations/org/quality_profiles/compare",
- "query": Object {
- "language": "java",
- "name": "Foo",
- },
+<React.Fragment>
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ download="foo.xml"
+ id="quality-profile-backup"
+ to="/api/qualityprofiles/backup?profileKey=foo"
+ >
+ backup_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-compare"
+ to={
+ Object {
+ "pathname": "/organizations/org/quality_profiles/compare",
+ "query": Object {
+ "language": "java",
+ "name": "Foo",
+ },
+ }
}
- }
- >
- compare
- </ActionsDropdownItem>
-</ActionsDropdown>
+ >
+ compare
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+</React.Fragment>
`;
exports[`renders with permission to edit only 1`] = `
-<ActionsDropdown>
- <ActionsDropdownItem
- id="quality-profile-activate-more-rules"
- to={
- Object {
- "pathname": "/organizations/org/rules",
- "query": Object {
- "activation": "false",
- "qprofile": "foo",
- },
+<React.Fragment>
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ id="quality-profile-activate-more-rules"
+ to={
+ Object {
+ "pathname": "/organizations/org/rules",
+ "query": Object {
+ "activation": "false",
+ "qprofile": "foo",
+ },
+ }
}
- }
- >
- quality_profiles.activate_more_rules
- </ActionsDropdownItem>
- <ActionsDropdownItem
- download="foo.xml"
- id="quality-profile-backup"
- to="/api/qualityprofiles/backup?profileKey=foo"
- >
- backup_verb
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-compare"
- to={
- Object {
- "pathname": "/organizations/org/quality_profiles/compare",
- "query": Object {
- "language": "java",
- "name": "Foo",
- },
+ >
+ quality_profiles.activate_more_rules
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ download="foo.xml"
+ id="quality-profile-backup"
+ to="/api/qualityprofiles/backup?profileKey=foo"
+ >
+ backup_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-compare"
+ to={
+ Object {
+ "pathname": "/organizations/org/quality_profiles/compare",
+ "query": Object {
+ "language": "java",
+ "name": "Foo",
+ },
+ }
}
- }
- >
- compare
- </ActionsDropdownItem>
- <ActionsDropdownItem
- id="quality-profile-rename"
- onClick={[Function]}
- >
- rename
- </ActionsDropdownItem>
-</ActionsDropdown>
+ >
+ compare
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-rename"
+ onClick={[Function]}
+ >
+ rename
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
index 80ef81fbb54..8a0750085a4 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
@@ -19,10 +19,10 @@
*/
import * as React from 'react';
import { IndexLink } from 'react-router';
-import * as classNames from 'classnames';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getProfilesPath, getProfilesForLanguagePath } from '../utils';
import Dropdown from '../../../components/controls/Dropdown';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
interface Props {
currentFilter?: string;
@@ -48,36 +48,31 @@ export default function ProfilesListHeader({ currentFilter, languages, organizat
return (
<header className="quality-profiles-list-header clearfix">
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('dropdown', { open })}>
- <a
- className="dropdown-toggle link-no-underline js-language-filter"
- href="#"
- onClick={onToggleClick}>
- {label}
- <i className="icon-dropdown little-spacer-left" />
- </a>
-
- <ul className="dropdown-menu">
- <li>
- <IndexLink to={getProfilesPath(organization)}>
- {translate('quality_profiles.all_profiles')}
+ <Dropdown
+ className="display-inline-block"
+ overlay={
+ <ul className="menu">
+ <li>
+ <IndexLink to={getProfilesPath(organization)}>
+ {translate('quality_profiles.all_profiles')}
+ </IndexLink>
+ </li>
+ {languages.map(language => (
+ <li key={language.key}>
+ <IndexLink
+ className="js-language-filter-option"
+ data-language={language.key}
+ to={getProfilesForLanguagePath(language.key, organization)}>
+ {language.name}
</IndexLink>
</li>
- {languages.map(language => (
- <li key={language.key}>
- <IndexLink
- className="js-language-filter-option"
- data-language={language.key}
- to={getProfilesForLanguagePath(language.key, organization)}>
- {language.name}
- </IndexLink>
- </li>
- ))}
- </ul>
- </div>
- )}
+ ))}
+ </ul>
+ }>
+ <a className="dropdown-toggle link-no-underline js-language-filter" href="#">
+ <span className="text-middle">{label}</span>
+ <DropdownIcon className="little-spacer-left text-middle" />
+ </a>
</Dropdown>
</header>
);
diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx
index e34bbf7e10e..dd4c07d9b84 100644
--- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx
+++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import ChangeLogLevelForm from './ChangeLogLevelForm';
import RestartForm from '../../../components/common/RestartForm';
import { getFileNameSuffix } from '../utils';
@@ -102,53 +101,52 @@ export default class PageActions extends React.PureComponent<Props, State> {
/>
</span>
{this.props.canDownloadLogs && (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('display-inline-block dropdown spacer-left', { open })}>
- <Button onClick={onToggleClick}>
- {translate('system.download_logs')}
- <i className="icon-dropdown little-spacer-left" />
- </Button>
- <ul className="dropdown-menu">
- <li>
- <a
- download="sonarqube_app.log"
- href={logsUrl + '?process=app'}
- id="logs-link"
- target="_blank">
- Main Process
- </a>
- </li>
- <li>
- <a
- download="sonarqube_ce.log"
- href={logsUrl + '?process=ce'}
- id="ce-logs-link"
- target="_blank">
- Compute Engine
- </a>
- </li>
- <li>
- <a
- download="sonarqube_es.log"
- href={logsUrl + '?process=es'}
- id="es-logs-link"
- target="_blank">
- Search Engine
- </a>
- </li>
- <li>
- <a
- download="sonarqube_web.log"
- href={logsUrl + '?process=web'}
- id="web-logs-link"
- target="_blank">
- Web Server
- </a>
- </li>
- </ul>
- </div>
- )}
+ <Dropdown
+ className="display-inline-block spacer-left"
+ overlay={
+ <ul className="menu">
+ <li>
+ <a
+ download="sonarqube_app.log"
+ href={logsUrl + '?process=app'}
+ id="logs-link"
+ target="_blank">
+ Main Process
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_ce.log"
+ href={logsUrl + '?process=ce'}
+ id="ce-logs-link"
+ target="_blank">
+ Compute Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_es.log"
+ href={logsUrl + '?process=es'}
+ id="es-logs-link"
+ target="_blank">
+ Search Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_web.log"
+ href={logsUrl + '?process=web'}
+ id="web-logs-link"
+ target="_blank">
+ Web Server
+ </a>
+ </li>
+ </ul>
+ }>
+ <Button>
+ {translate('system.download_logs')}
+ <i className="icon-dropdown little-spacer-left" />
+ </Button>
</Dropdown>
)}
<a
diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx
index 44f9d93a7a8..6190c2ed029 100644
--- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx
@@ -29,7 +29,7 @@ jest.mock('../../utils', () => ({
it('should render correctly', () => {
const wrapper = getWrapper({ serverId: 'MyServerId' });
expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('should render without restart and log download', () => {
diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap
index 3c698c9ba19..14a070daf60 100644
--- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap
@@ -22,7 +22,62 @@ exports[`should render correctly 1`] = `
onClick={[Function]}
/>
</span>
- <Dropdown />
+ <Dropdown
+ className="display-inline-block spacer-left"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <a
+ download="sonarqube_app.log"
+ href="/api/system/logs?process=app"
+ id="logs-link"
+ target="_blank"
+ >
+ Main Process
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_ce.log"
+ href="/api/system/logs?process=ce"
+ id="ce-logs-link"
+ target="_blank"
+ >
+ Compute Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_es.log"
+ href="/api/system/logs?process=es"
+ id="es-logs-link"
+ target="_blank"
+ >
+ Search Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_web.log"
+ href="/api/system/logs?process=web"
+ id="web-logs-link"
+ target="_blank"
+ >
+ Web Server
+ </a>
+ </li>
+ </ul>
+ }
+ >
+ <Button>
+ system.download_logs
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </Button>
+ </Dropdown>
<a
className="button spacer-left"
download="sonarqube-support-info-filesuffix(MyServerId).json"
@@ -44,62 +99,62 @@ exports[`should render correctly 1`] = `
`;
exports[`should render correctly 2`] = `
-<div
- className="display-inline-block dropdown spacer-left"
+<Dropdown
+ className="display-inline-block spacer-left"
+ overlay={
+ <ul
+ className="menu"
+ >
+ <li>
+ <a
+ download="sonarqube_app.log"
+ href="/api/system/logs?process=app"
+ id="logs-link"
+ target="_blank"
+ >
+ Main Process
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_ce.log"
+ href="/api/system/logs?process=ce"
+ id="ce-logs-link"
+ target="_blank"
+ >
+ Compute Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_es.log"
+ href="/api/system/logs?process=es"
+ id="es-logs-link"
+ target="_blank"
+ >
+ Search Engine
+ </a>
+ </li>
+ <li>
+ <a
+ download="sonarqube_web.log"
+ href="/api/system/logs?process=web"
+ id="web-logs-link"
+ target="_blank"
+ >
+ Web Server
+ </a>
+ </li>
+ </ul>
+ }
>
- <Button
- onClick={[Function]}
- >
+ <Button>
system.download_logs
<i
className="icon-dropdown little-spacer-left"
/>
</Button>
- <ul
- className="dropdown-menu"
- >
- <li>
- <a
- download="sonarqube_app.log"
- href="/api/system/logs?process=app"
- id="logs-link"
- target="_blank"
- >
- Main Process
- </a>
- </li>
- <li>
- <a
- download="sonarqube_ce.log"
- href="/api/system/logs?process=ce"
- id="ce-logs-link"
- target="_blank"
- >
- Compute Engine
- </a>
- </li>
- <li>
- <a
- download="sonarqube_es.log"
- href="/api/system/logs?process=es"
- id="es-logs-link"
- target="_blank"
- >
- Search Engine
- </a>
- </li>
- <li>
- <a
- download="sonarqube_web.log"
- href="/api/system/logs?process=web"
- id="web-logs-link"
- target="_blank"
- >
- Web Server
- </a>
- </li>
- </ul>
-</div>
+</Dropdown>
`;
exports[`should render without restart and log download 1`] = `
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
index ccd1471a245..5c132436de1 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
@@ -49,7 +49,7 @@ export default class UserActions extends React.PureComponent<Props, State> {
renderActions = () => {
const { user } = this.props;
return (
- <ActionsDropdown menuClassName="dropdown-menu-right">
+ <ActionsDropdown>
<ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
{translate('update_details')}
</ActionsDropdownItem>
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap
index 0fd31e7f6ae..9b8a3267c76 100644
--- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap
@@ -2,9 +2,7 @@
exports[`should render correctly 1`] = `
<React.Fragment>
- <ActionsDropdown
- menuClassName="dropdown-menu-right"
- >
+ <ActionsDropdown>
<ActionsDropdownItem
className="js-user-update"
onClick={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx
new file mode 100644
index 00000000000..4e177160af0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Webhook } from '../../../app/types';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ onClose: () => void;
+ onSubmit: () => Promise<void>;
+ webhook: Webhook;
+}
+
+export default function DeleteWebhookForm({ onClose, onSubmit, webhook }: Props) {
+ const header = translate('webhooks.delete');
+
+ return (
+ <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters('webhooks.delete.confirm', webhook.name)}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
index 0027720ad60..cfbeff267e3 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
@@ -19,13 +19,13 @@
*/
import * as React from 'react';
import CreateWebhookForm from './CreateWebhookForm';
+import DeleteWebhookForm from './DeleteWebhookForm';
import DeliveriesForm from './DeliveriesForm';
import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
} from '../../../components/controls/ActionsDropdown';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
import { Webhook } from '../../../app/types';
interface Props {
@@ -35,13 +35,14 @@ interface Props {
}
interface State {
+ deleting: boolean;
deliveries: boolean;
updating: boolean;
}
export default class WebhookActions extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { deliveries: false, updating: false };
+ state: State = { deleting: false, deliveries: false, updating: false };
componentDidMount() {
this.mounted = true;
@@ -55,6 +56,16 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
return this.props.onDelete(this.props.webhook.key);
};
+ handleDeleteClick = () => {
+ this.setState({ deleting: true });
+ };
+
+ handleDeletingStop = () => {
+ if (this.mounted) {
+ this.setState({ deleting: false });
+ }
+ };
+
handleDeliveriesClick = () => {
this.setState({ deliveries: true });
};
@@ -91,25 +102,18 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
</ActionsDropdownItem>
)}
<ActionsDropdownDivider />
- <ConfirmButton
- confirmButtonText={translate('delete')}
- isDestructive={true}
- modalBody={translateWithParameters('webhooks.delete.confirm', webhook.name)}
- modalHeader={translate('webhooks.delete')}
- onConfirm={this.handleDelete}>
- {({ onClick }) => (
- <ActionsDropdownItem
- className="js-webhook-delete"
- destructive={true}
- onClick={onClick}>
- {translate('delete')}
- </ActionsDropdownItem>
- )}
- </ConfirmButton>
+ <ActionsDropdownItem
+ className="js-webhook-delete"
+ destructive={true}
+ onClick={this.handleDeleteClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
</ActionsDropdown>
+
{this.state.deliveries && (
<DeliveriesForm onClose={this.handleDeliveriesStop} webhook={webhook} />
)}
+
{this.state.updating && (
<CreateWebhookForm
onClose={this.handleUpdatingStop}
@@ -117,6 +121,14 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
webhook={webhook}
/>
)}
+
+ {this.state.deleting && (
+ <DeleteWebhookForm
+ onClose={this.handleDeletingStop}
+ onSubmit={this.handleDelete}
+ webhook={webhook}
+ />
+ )}
</>
);
}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
index 70fe2bc5c77..8e1fd5d57f4 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
@@ -55,14 +55,9 @@ it('should display the update webhook form', () => {
it('should display the delete webhook form', () => {
const onDelete = jest.fn(() => Promise.resolve());
const wrapper = getWrapper({ onDelete });
- click(
- wrapper
- .find('ConfirmButton')
- .dive()
- .find('.js-webhook-delete')
- );
- expect(wrapper.find('ConfirmButton').exists()).toBeTruthy();
- wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ click(wrapper.find('.js-webhook-delete'));
+ expect(wrapper.find('DeleteWebhookForm').exists()).toBeTruthy();
+ wrapper.find('DeleteWebhookForm').prop<Function>('onSubmit')();
expect(onDelete).lastCalledWith(webhook.key);
});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
index 7079ff776af..f72bc992de3 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
@@ -18,13 +18,13 @@ exports[`should display the deliveries form 1`] = `
webhooks.deliveries.show
</ActionsDropdownItem>
<ActionsDropdownDivider />
- <ConfirmButton
- confirmButtonText="delete"
- isDestructive={true}
- modalBody="webhooks.delete.confirm.foo"
- modalHeader="webhooks.delete"
- onConfirm={[Function]}
- />
+ <ActionsDropdownItem
+ className="js-webhook-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
</ActionsDropdown>
</React.Fragment>
`;
@@ -41,13 +41,13 @@ exports[`should render correctly 1`] = `
update_verb
</ActionsDropdownItem>
<ActionsDropdownDivider />
- <ConfirmButton
- confirmButtonText="delete"
- isDestructive={true}
- modalBody="webhooks.delete.confirm.foo"
- modalHeader="webhooks.delete"
- onConfirm={[Function]}
- />
+ <ActionsDropdownItem
+ className="js-webhook-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
</ActionsDropdown>
</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
index 041856e2426..1499a05acb0 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
@@ -21,7 +21,6 @@ import { stringify } from 'querystring';
import * as React from 'react';
import { Link } from 'react-router';
import * as PropTypes from 'prop-types';
-import * as classNames from 'classnames';
import MeasuresOverlay from './components/MeasuresOverlay';
import { SourceViewerFile, BranchLike } from '../../app/types';
import QualifierIcon from '../shared/QualifierIcon';
@@ -29,6 +28,7 @@ import Dropdown from '../controls/Dropdown';
import FavoriteContainer from '../controls/FavoriteContainer';
import ListIcon from '../icons-components/ListIcon';
import { ButtonIcon } from '../ui/buttons';
+import { PopupPlacement } from '../ui/popups';
import { WorkspaceContext } from '../workspace/context';
import {
getPathUrlAsString,
@@ -128,56 +128,54 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
</div>
</div>
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('dropdown source-viewer-header-actions', { open })}>
- <ButtonIcon
- className="js-actions"
- onClick={onToggleClick}
- tooltip={translate('component_viewer.more_actions')}>
- <ListIcon />
- </ButtonIcon>
- <ul className="dropdown-menu dropdown-menu-right">
- <li>
- <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}>
- {translate('component_viewer.show_details')}
- </a>
- {this.state.measuresOverlay && (
- <MeasuresOverlay
- branchLike={this.props.branchLike}
- onClose={this.handleMeasuresOverlayClose}
- sourceViewerFile={this.props.sourceViewerFile}
- />
- )}
- </li>
- <li>
- <a
- className="js-new-window"
- href={getPathUrlAsString({
- pathname: '/component',
- query: { id: key, ...getBranchLikeQuery(this.props.branchLike) }
- })}
- target="_blank">
- {translate('component_viewer.new_window')}
- </a>
- </li>
- {!workspace && (
- <li>
- <a className="js-workspace" href="#" onClick={this.openInWorkspace}>
- {translate('component_viewer.open_in_workspace')}
- </a>
- </li>
- )}
+ <Dropdown
+ className="source-viewer-header-actions"
+ overlay={
+ <ul className="menu">
+ <li>
+ <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}>
+ {translate('component_viewer.show_details')}
+ </a>
+ </li>
+ <li>
+ <a
+ className="js-new-window"
+ href={getPathUrlAsString({
+ pathname: '/component',
+ query: { id: key, ...getBranchLikeQuery(this.props.branchLike) }
+ })}
+ target="_blank">
+ {translate('component_viewer.new_window')}
+ </a>
+ </li>
+ {!workspace && (
<li>
- <a className="js-raw-source" href={rawSourcesLink} target="_blank">
- {translate('component_viewer.show_raw_source')}
+ <a className="js-workspace" href="#" onClick={this.openInWorkspace}>
+ {translate('component_viewer.open_in_workspace')}
</a>
</li>
- </ul>
- </div>
- )}
+ )}
+ <li>
+ <a className="js-raw-source" href={rawSourcesLink} target="_blank">
+ {translate('component_viewer.show_raw_source')}
+ </a>
+ </li>
+ </ul>
+ }
+ overlayPlacement={PopupPlacement.BottomRight}>
+ <ButtonIcon className="js-actions">
+ <ListIcon />
+ </ButtonIcon>
</Dropdown>
+ {this.state.measuresOverlay && (
+ <MeasuresOverlay
+ branchLike={this.props.branchLike}
+ onClose={this.handleMeasuresOverlayClose}
+ sourceViewerFile={this.props.sourceViewerFile}
+ />
+ )}
+
<div className="source-viewer-header-measures">
{isUnitTest && (
<div className="source-viewer-header-measure">
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
index 8e5aeca1727..618e258da8b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
@@ -22,8 +22,9 @@ import { groupBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { getTests } from '../../../api/components';
import { BranchLike, SourceLine, TestCase } from '../../../app/types';
-import BubblePopup from '../../common/BubblePopup';
+import { DropdownOverlay } from '../../controls/Dropdown';
import TestStatusIcon from '../../shared/TestStatusIcon';
+import { PopupPlacement } from '../../ui/popups';
import { WorkspaceContext } from '../../workspace/context';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
@@ -34,7 +35,6 @@ interface Props {
componentKey: string;
line: SourceLine;
onClose: () => void;
- popupPosition?: any;
}
interface State {
@@ -114,53 +114,55 @@ export default class CoveragePopup extends React.PureComponent<Props, State> {
});
return (
- <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
- <div className="bubble-popup-title">
- {translate('source_viewer.covered')}
- {!!line.conditions && (
- <div>
- {'('}
- {line.coveredConditions || '0'}
- {' of '}
- {line.conditions} {translate('source_viewer.conditions')}
- {')'}
- </div>
+ <DropdownOverlay placement={PopupPlacement.RightTop}>
+ <div className="abs-width-400">
+ <h6 className="spacer-bottom">
+ {translate('source_viewer.covered')}
+ {!!line.conditions && (
+ <div>
+ {'('}
+ {line.coveredConditions || '0'}
+ {' of '}
+ {line.conditions} {translate('source_viewer.conditions')}
+ {')'}
+ </div>
+ )}
+ </h6>
+ {this.state.loading ? (
+ <i className="spinner" />
+ ) : (
+ <>
+ {testFiles.length === 0 &&
+ translate('source_viewer.tooltip.no_information_about_tests')}
+ {testFiles.map(testFile => (
+ <div className="spacer-top text-ellipsis" key={testFile.file.key}>
+ <a
+ data-key={testFile.file.key}
+ href="#"
+ onClick={this.handleTestClick}
+ title={testFile.file.longName}>
+ <span>{collapsePath(testFile.file.longName)}</span>
+ </a>
+ <ul>
+ {testFile.tests.map(testCase => (
+ <li
+ className="display-flex-center little-spacer-top"
+ key={testCase.id}
+ title={testCase.name}>
+ <TestStatusIcon className="spacer-right" status={testCase.status} />
+ <div className="display-inline-block text-ellipsis">{testCase.name}</div>
+ {testCase.status !== 'SKIPPED' && (
+ <span className="spacer-left note">{testCase.durationInMs}ms</span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ ))}
+ </>
)}
</div>
- {this.state.loading ? (
- <i className="spinner" />
- ) : (
- <>
- {testFiles.length === 0 &&
- translate('source_viewer.tooltip.no_information_about_tests')}
- {testFiles.map(testFile => (
- <div className="bubble-popup-section" key={testFile.file.key}>
- <a
- data-key={testFile.file.key}
- href="#"
- onClick={this.handleTestClick}
- title={testFile.file.longName}>
- <span>{collapsePath(testFile.file.longName)}</span>
- </a>
- <ul className="bubble-popup-list">
- {testFile.tests.map(testCase => (
- <li
- className="component-viewer-popup-test"
- key={testCase.id}
- title={testCase.name}>
- <TestStatusIcon className="spacer-right" status={testCase.status} />
- {testCase.name}
- {testCase.status !== 'SKIPPED' && (
- <span className="spacer-left note">{testCase.durationInMs}ms</span>
- )}
- </li>
- ))}
- </ul>
- </div>
- ))}
- </>
- )}
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
index 163648dd6e2..1322545356f 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
@@ -22,8 +22,9 @@ import { Link } from 'react-router';
import * as PropTypes from 'prop-types';
import { groupBy, sortBy } from 'lodash';
import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types';
-import BubblePopup from '../../common/BubblePopup';
+import { DropdownOverlay } from '../../controls/Dropdown';
import QualifierIcon from '../../shared/QualifierIcon';
+import { PopupPlacement } from '../../ui/popups';
import { WorkspaceContext } from '../../workspace/context';
import { translate } from '../../../helpers/l10n';
import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path';
@@ -90,8 +91,8 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
);
return (
- <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
- <div className="bubble-popup-container">
+ <DropdownOverlay placement={PopupPlacement.RightTop}>
+ <div className="source-viewer-bubble-popup abs-width-400">
{this.props.inRemovedComponent && (
<div className="alert alert-warning">
{translate('duplications.dups_found_on_deleted_resource')}
@@ -99,11 +100,11 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
)}
{duplications.length > 0 && (
<>
- <div className="bubble-popup-title">
+ <h6 className="spacer-bottom">
{translate('component_viewer.transition.duplication')}
- </div>
+ </h6>
{duplications.map(duplication => (
- <div className="bubble-popup-section" key={duplication.file.key}>
+ <div className="spacer-top text-ellipsis" key={duplication.file.key}>
<div className="component-name">
{this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
<>
@@ -164,7 +165,7 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
</>
)}
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
index 17d98a03eca..be4c2d1a842 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
@@ -21,8 +21,8 @@ import * as React from 'react';
import CoveragePopup from './CoveragePopup';
import { BranchLike, SourceLine } from '../../../app/types';
import Tooltip from '../../controls/Tooltip';
+import Toggler from '../../controls/Toggler';
import { translate } from '../../../helpers/l10n';
-import BubblePopupHelper from '../../common/BubblePopupHelper';
interface Props {
branchLike: BranchLike | undefined;
@@ -59,7 +59,9 @@ export default class LineCoverage extends React.PureComponent<Props> {
line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered';
const cell = line.coverageStatus ? (
- <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right">
+ <Tooltip
+ overlay={popupOpen ? undefined : translate('source_viewer.tooltip', line.coverageStatus)}
+ placement="right">
<div className="source-line-bar" />
</Tooltip>
) : (
@@ -75,20 +77,19 @@ export default class LineCoverage extends React.PureComponent<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={0}>
- {cell}
- <BubblePopupHelper
- isOpen={popupOpen}
- popup={
+ <Toggler
+ onRequestClose={this.closePopup}
+ open={popupOpen}
+ overlay={
<CoveragePopup
branchLike={branchLike}
componentKey={componentKey}
line={line}
onClose={this.closePopup}
/>
- }
- position="bottomright"
- togglePopup={this.handleTogglePopup}
- />
+ }>
+ {cell}
+ </Toggler>
</td>
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
index 0121bfa868d..93772dcbb72 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
@@ -21,8 +21,8 @@ import * as React from 'react';
import * as classNames from 'classnames';
import { SourceLine } from '../../../app/types';
import Tooltip from '../../controls/Tooltip';
+import Toggler from '../../controls/Toggler';
import { translate } from '../../../helpers/l10n';
-import BubblePopupHelper from '../../common/BubblePopupHelper';
interface Props {
duplicated: boolean;
@@ -54,6 +54,10 @@ export default class LineDuplicationBlock extends React.PureComponent<Props> {
});
};
+ closePopup = () => {
+ this.handleTogglePopup(false);
+ };
+
render() {
const { duplicated, index, line, popupOpen } = this.props;
const className = classNames('source-meta', 'source-line-duplications-extra', {
@@ -71,15 +75,14 @@ export default class LineDuplicationBlock extends React.PureComponent<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={0}>
- <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
- {cell}
- </Tooltip>
- <BubblePopupHelper
- isOpen={popupOpen}
- popup={this.props.renderDuplicationPopup(index, line.line)}
- position="bottomright"
- togglePopup={this.handleTogglePopup}
- />
+ <Toggler
+ onRequestClose={this.closePopup}
+ open={popupOpen}
+ overlay={this.props.renderDuplicationPopup(index, line.line)}>
+ <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
+ {cell}
+ </Tooltip>
+ </Toggler>
</td>
) : (
<td className={className} data-index={index} data-line-number={line.line}>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
index 9297036d88d..15af9ac258d 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import LineOptionsPopup from './LineOptionsPopup';
import { BranchLike, SourceLine } from '../../../app/types';
-import BubblePopupHelper from '../../common/BubblePopupHelper';
+import Toggler from '../../controls/Toggler';
interface Props {
branchLike: BranchLike | undefined;
@@ -42,6 +42,10 @@ export default class LineNumber extends React.PureComponent<Props> {
this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open });
};
+ closePopup = () => {
+ this.handleTogglePopup(false);
+ };
+
render() {
const { branchLike, componentKey, line, popupOpen } = this.props;
const { line: lineNumber } = line;
@@ -54,14 +58,12 @@ export default class LineNumber extends React.PureComponent<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={0}>
- <BubblePopupHelper
- isOpen={popupOpen}
- offset={{ vertical: -18, horizontal: 0 }}
- popup={
+ <Toggler
+ onRequestClose={this.closePopup}
+ open={popupOpen}
+ overlay={
<LineOptionsPopup branchLike={branchLike} componentKey={componentKey} line={line} />
}
- position="bottomright"
- togglePopup={this.handleTogglePopup}
/>
</td>
) : (
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
index 340304bb280..34a136d8391 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
@@ -20,7 +20,8 @@
import * as React from 'react';
import { Link } from 'react-router';
import { BranchLike, SourceLine } from '../../../app/types';
-import BubblePopup from '../../common/BubblePopup';
+import { DropdownOverlay } from '../../controls/Dropdown';
+import { PopupPlacement } from '../../ui/popups';
import { translate } from '../../../helpers/l10n';
import { getBranchLikeQuery } from '../../../helpers/branches';
@@ -28,21 +29,20 @@ interface Props {
branchLike: BranchLike | undefined;
componentKey: string;
line: SourceLine;
- popupPosition?: any;
}
-export default function LineOptionsPopup({ branchLike, componentKey, line, popupPosition }: Props) {
+export default function LineOptionsPopup({ branchLike, componentKey, line }: Props) {
const permalink = {
pathname: '/component',
query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) }
};
return (
- <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
- <div className="bubble-popup-section">
+ <DropdownOverlay placement={PopupPlacement.RightTop}>
+ <div className="source-viewer-bubble-popup nowrap">
<Link className="js-get-permalink" to={permalink}>
{translate('component_viewer.get_permalink')}
</Link>
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
index e692a3c46f1..83c326a12ea 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import SCMPopup from './SCMPopup';
import { SourceLine } from '../../../app/types';
-import BubblePopupHelper from '../../common/BubblePopupHelper';
+import Toggler from '../../controls/Toggler';
interface Props {
line: SourceLine;
@@ -41,6 +41,10 @@ export default class LineSCM extends React.PureComponent<Props> {
this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open });
};
+ closePopup = () => {
+ this.handleTogglePopup(false);
+ };
+
render() {
const { line, popupOpen, previousLine } = this.props;
const hasPopup = !!line.line;
@@ -55,14 +59,12 @@ export default class LineSCM extends React.PureComponent<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={0}>
- {cell}
- <BubblePopupHelper
- isOpen={popupOpen}
- offset={{ vertical: -18, horizontal: 0 }}
- popup={<SCMPopup line={line} />}
- position="bottomright"
- togglePopup={this.handleTogglePopup}
- />
+ <Toggler
+ onRequestClose={this.closePopup}
+ open={popupOpen}
+ overlay={<SCMPopup line={line} />}>
+ {cell}
+ </Toggler>
</td>
) : (
<td className="source-meta source-line-scm" data-line-number={line.line}>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
index f89a9aa408b..1e46274b9e9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
@@ -81,12 +81,12 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Pro
testCase.status !== 'FAILURE' &&
coveredFiles !== undefined && (
<>
- <div className="bubble-popup-title">
+ <h6 className="spacer-bottom">
{translate('component_viewer.transition.covers')}
- </div>
+ </h6>
{coveredFiles.length > 0
? coveredFiles.map(coveredFile => (
- <div className="bubble-popup-section" key={coveredFile.key}>
+ <div className="spacer-top" key={coveredFile.key}>
<Link to={getProjectUrl(coveredFile.key)}>{coveredFile.longName}</Link>
<span className="note spacer-left">
{translateWithParameters(
@@ -102,7 +102,7 @@ export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Pro
{testCase.status !== 'OK' && (
<>
- <div className="bubble-popup-title">{translate('component_viewer.details')}</div>
+ <h6 className="spacer-bottom">{translate('component_viewer.details')}</h6>
{testCase.message && <pre>{testCase.message}</pre>}
<pre>{testCase.stacktrace}</pre>
</>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
index 1a94d48e657..fa1b766c12a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
@@ -19,24 +19,26 @@
*/
import * as React from 'react';
import { SourceLine } from '../../../app/types';
-import BubblePopup from '../../common/BubblePopup';
+import { DropdownOverlay } from '../../controls/Dropdown';
import DateFormatter from '../../intl/DateFormatter';
+import { PopupPlacement } from '../../ui/popups';
interface Props {
line: SourceLine;
- popupPosition?: any;
}
-export default function SCMPopup({ line, popupPosition }: Props) {
+export default function SCMPopup({ line }: Props) {
return (
- <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
- <div className="bubble-popup-section">{line.scmAuthor}</div>
- {line.scmDate && (
- <div className="bubble-popup-section">
- <DateFormatter date={line.scmDate} />
- </div>
- )}
- {line.scmRevision && <div className="bubble-popup-section">{line.scmRevision}</div>}
- </BubblePopup>
+ <DropdownOverlay placement={PopupPlacement.RightTop}>
+ <div className="source-viewer-bubble-popup abs-width-400">
+ <div>{line.scmAuthor}</div>
+ {line.scmDate && (
+ <div className="spacer-top">
+ <DateFormatter date={line.scmDate} />
+ </div>
+ )}
+ {line.scmRevision && <div className="spacer-top">{line.scmRevision}</div>}
+ </div>
+ </DropdownOverlay>
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
index 4e01802b363..e715b7ded52 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
@@ -8,17 +8,10 @@ exports[`render covered line 1`] = `
role="button"
tabIndex={0}
>
- <Tooltip
- overlay="source_viewer.tooltip.covered"
- placement="right"
- >
- <div
- className="source-line-bar"
- />
- </Tooltip>
- <BubblePopupHelper
- isOpen={false}
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<CoveragePopup
branchLike={undefined}
componentKey="foo"
@@ -31,9 +24,16 @@ exports[`render covered line 1`] = `
onClose={[Function]}
/>
}
- position="bottomright"
- togglePopup={[Function]}
- />
+ >
+ <Tooltip
+ overlay="source_viewer.tooltip.covered"
+ placement="right"
+ >
+ <div
+ className="source-line-bar"
+ />
+ </Tooltip>
+ </Toggler>
</td>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
index 9ea77716f15..7147eca868e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
@@ -9,19 +9,19 @@ exports[`render duplicated line 1`] = `
role="button"
tabIndex={0}
>
- <Tooltip
- overlay="source_viewer.tooltip.duplicated_block"
- placement="right"
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
>
- <div
- className="source-line-bar"
- />
- </Tooltip>
- <BubblePopupHelper
- isOpen={false}
- position="bottomright"
- togglePopup={[Function]}
- />
+ <Tooltip
+ overlay="source_viewer.tooltip.duplicated_block"
+ placement="right"
+ >
+ <div
+ className="source-line-bar"
+ />
+ </Tooltip>
+ </Toggler>
</td>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
index 3841048e4ae..f5d3c9deff3 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
@@ -14,15 +14,10 @@ exports[`render line 3 1`] = `
role="button"
tabIndex={0}
>
- <BubblePopupHelper
- isOpen={false}
- offset={
- Object {
- "horizontal": 0,
- "vertical": -18,
- }
- }
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<LineOptionsPopup
branchLike={undefined}
componentKey="foo"
@@ -33,8 +28,6 @@ exports[`render line 3 1`] = `
}
/>
}
- position="bottomright"
- togglePopup={[Function]}
/>
</td>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
index 13d7b6ee859..96b360bb80a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render 1`] = `
-<BubblePopup
- customClass="source-viewer-bubble-popup"
+<DropdownOverlay
+ placement="right-top"
>
<div
- className="bubble-popup-section"
+ className="source-viewer-bubble-popup nowrap"
>
<Link
className="js-get-permalink"
@@ -25,5 +25,5 @@ exports[`should render 1`] = `
component_viewer.get_permalink
</Link>
</div>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
index c320b6a9b36..dac002460eb 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
@@ -8,15 +8,10 @@ exports[`does not render scm details 1`] = `
role="button"
tabIndex={0}
>
- <BubblePopupHelper
- isOpen={false}
- offset={
- Object {
- "horizontal": 0,
- "vertical": -18,
- }
- }
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<SCMPopup
line={
Object {
@@ -27,8 +22,6 @@ exports[`does not render scm details 1`] = `
}
/>
}
- position="bottomright"
- togglePopup={[Function]}
/>
</td>
`;
@@ -41,19 +34,10 @@ exports[`render scm details 1`] = `
role="button"
tabIndex={0}
>
- <div
- className="source-line-scm-inner"
- data-author="foo"
- />
- <BubblePopupHelper
- isOpen={false}
- offset={
- Object {
- "horizontal": 0,
- "vertical": -18,
- }
- }
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<SCMPopup
line={
Object {
@@ -64,9 +48,12 @@ exports[`render scm details 1`] = `
}
/>
}
- position="bottomright"
- togglePopup={[Function]}
- />
+ >
+ <div
+ className="source-line-scm-inner"
+ data-author="foo"
+ />
+ </Toggler>
</td>
`;
@@ -78,19 +65,10 @@ exports[`render scm details for the first line 1`] = `
role="button"
tabIndex={0}
>
- <div
- className="source-line-scm-inner"
- data-author="foo"
- />
- <BubblePopupHelper
- isOpen={false}
- offset={
- Object {
- "horizontal": 0,
- "vertical": -18,
- }
- }
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<SCMPopup
line={
Object {
@@ -101,8 +79,11 @@ exports[`render scm details for the first line 1`] = `
}
/>
}
- position="bottomright"
- togglePopup={[Function]}
- />
+ >
+ <div
+ className="source-line-scm-inner"
+ data-author="foo"
+ />
+ </Toggler>
</td>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
index 43ac2de8f8e..b053a1bf9af 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
@@ -12,11 +12,11 @@ exports[`should render ERROR test 1`] = `
className="source-viewer-measures-card source-viewer-measures-card-fixed-height"
>
<React.Fragment>
- <div
- className="bubble-popup-title"
+ <h6
+ className="spacer-bottom"
>
component_viewer.details
- </div>
+ </h6>
<pre>
Something failed
</pre>
@@ -39,13 +39,13 @@ exports[`should render OK test 1`] = `
className="source-viewer-measures-card source-viewer-measures-card-fixed-height"
>
<React.Fragment>
- <div
- className="bubble-popup-title"
+ <h6
+ className="spacer-bottom"
>
component_viewer.transition.covers
- </div>
+ </h6>
<div
- className="bubble-popup-section"
+ className="spacer-top"
key="project:src/file.js"
>
<Link
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
index d8460698802..99378cb457d 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
@@ -1,20 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render 1`] = `
-<BubblePopup
- customClass="source-viewer-bubble-popup"
+<DropdownOverlay
+ placement="right-top"
>
<div
- className="bubble-popup-section"
+ className="source-viewer-bubble-popup abs-width-400"
>
- foo
+ <div>
+ foo
+ </div>
+ <div
+ className="spacer-top"
+ >
+ <DateFormatter
+ date="2017-01-01"
+ />
+ </div>
</div>
- <div
- className="bubble-popup-section"
- >
- <DateFormatter
- date="2017-01-01"
- />
- </div>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
index 60a57ab69ba..839838acfe9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css
+++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
@@ -396,10 +396,6 @@
clear: both;
}
-.source-viewer-measures .bubble-popup-section {
- width: 100%;
-}
-
.source-viewer-measures + .source-viewer-measures {
margin-top: 40px;
}
@@ -508,9 +504,6 @@
}
.source-viewer-bubble-popup {
- top: -16px;
- left: 100%;
- width: 480px;
font-family: var(--baseFontFamily);
font-size: var(--baseFontSize);
text-align: left;
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
index 2723b9dad7f..f4d54a0aa5f 100644
--- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
+++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
@@ -251,24 +251,15 @@ export default class AdvancedTimeline extends React.PureComponent {
handleMouseOut = (evt /*: Event & { relatedTarget: HTMLElement } */) => {
const { updateTooltip } = this.props;
- const targetClass =
- evt.relatedTarget && typeof evt.relatedTarget.className === 'string'
- ? evt.relatedTarget.className
- : '';
- if (
- !updateTooltip ||
- targetClass.includes('bubble-popup') ||
- targetClass.includes('graph-tooltip')
- ) {
- return;
+ if (updateTooltip) {
+ this.setState({
+ mouseOver: false,
+ selectedDate: null,
+ selectedDateXPos: null,
+ selectedDateIdx: null
+ });
+ updateTooltip(null, null, null);
}
- this.setState({
- mouseOver: false,
- selectedDate: null,
- selectedDateXPos: null,
- selectedDateIdx: null
- });
- updateTooltip(null, null, null);
};
handleClick = () => {
diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
deleted file mode 100644
index 9f66d022cc9..00000000000
--- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import * as classNames from 'classnames';
-import { BubblePopupPosition } from './BubblePopup';
-
-interface Props {
- className?: string;
- children?: React.ReactNode;
- isOpen: boolean;
- offset?: { vertical: number; horizontal: number };
- popup: JSX.Element;
- position: 'bottomleft' | 'bottomright';
- togglePopup: (show: boolean) => void;
-}
-
-interface State {
- position: BubblePopupPosition;
-}
-
-export default class BubblePopupHelper extends React.PureComponent<Props, State> {
- container?: HTMLElement | null;
- popupContainer?: HTMLElement | null;
- state: State = {
- position: { top: 0, right: 0 }
- };
-
- componentDidMount() {
- this.setState({ position: this.getPosition(this.props) });
- }
-
- componentWillReceiveProps(nextProps: Props) {
- if (!this.props.isOpen && nextProps.isOpen) {
- window.addEventListener('keydown', this.handleKey, false);
- window.addEventListener('click', this.handleOutsideClick, false);
- } else if (this.props.isOpen && !nextProps.isOpen) {
- window.removeEventListener('keydown', this.handleKey);
- window.removeEventListener('click', this.handleOutsideClick);
- }
- }
-
- handleKey = (event: KeyboardEvent) => {
- // Escape key
- if (event.keyCode === 27) {
- this.props.togglePopup(false);
- }
- };
-
- handleOutsideClick = (event: MouseEvent) => {
- if (!this.popupContainer || !this.popupContainer.contains(event.target as Node)) {
- this.props.togglePopup(false);
- }
- };
-
- handleClick(event: React.SyntheticEvent<HTMLElement>) {
- event.stopPropagation();
- }
-
- getPosition(props: Props) {
- if (this.container) {
- const containerPos = this.container.getBoundingClientRect();
- const { position } = props;
- const offset = props.offset || { vertical: 0, horizontal: 0 };
- if (position === 'bottomleft') {
- return { top: containerPos.height + offset.vertical, left: offset.horizontal };
- } else {
- // if (position === 'bottomright')
- return { top: containerPos.height + offset.vertical, right: offset.horizontal };
- }
- } else {
- return { top: 0, right: 0 };
- }
- }
-
- render() {
- return (
- <div
- className={classNames(this.props.className, 'bubble-popup-helper')}
- onClick={this.handleClick}
- ref={container => (this.container = container)}
- role="tooltip"
- tabIndex={0}>
- {this.props.children}
- {this.props.isOpen && (
- <div ref={popupContainer => (this.popupContainer = popupContainer)}>
- {React.cloneElement(this.props.popup, {
- popupPosition: this.state.position
- })}
- </div>
- )}
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js
deleted file mode 100644
index ffd00e6cb84..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow, mount } from 'enzyme';
-import React from 'react';
-import BubblePopupHelper from '../BubblePopupHelper';
-import BubblePopup from '../BubblePopup';
-import { click } from '../../../helpers/testUtils';
-
-it('should render an open popup on the right', () => {
- const toggle = jest.fn();
- const popup = shallow(
- <BubblePopupHelper
- isOpen={true}
- position="bottomright"
- togglePopup={toggle}
- popup={
- <BubblePopup>
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>,
- { disableLifecycleMethods: true }
- );
- expect(popup).toMatchSnapshot();
-});
-
-it('should render the popup helper with a closed popup', () => {
- const toggle = jest.fn();
- const popup = shallow(
- <BubblePopupHelper
- isOpen={false}
- position="bottomright"
- togglePopup={toggle}
- popup={
- <BubblePopup>
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>,
- { disableLifecycleMethods: true }
- );
- expect(popup).toMatchSnapshot();
-});
-
-it('should render with custom classes', () => {
- const toggle = jest.fn();
- const popup = shallow(
- <BubblePopupHelper
- customClass="myhelperclass"
- isOpen={true}
- position="bottomright"
- togglePopup={toggle}
- popup={
- <BubblePopup customClass="mypopupclass">
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>,
- { disableLifecycleMethods: true }
- );
- expect(popup).toMatchSnapshot();
-});
-
-it('should render the popup with offset', () => {
- const toggle = jest.fn();
- const popup = mount(
- <BubblePopupHelper
- isOpen={true}
- offset={{ vertical: 5, horizontal: 2 }}
- position="bottomright"
- togglePopup={toggle}
- popup={
- <BubblePopup>
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>
- );
- expect(popup.find('BubblePopup')).toMatchSnapshot();
-});
-
-it('should render an open popup on the left', () => {
- const toggle = jest.fn();
- const popup = mount(
- <BubblePopupHelper
- isOpen={true}
- offset={{ vertical: 0, horizontal: 2 }}
- position="bottomleft"
- togglePopup={toggle}
- popup={
- <BubblePopup>
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>
- );
- expect(popup.find('BubblePopup')).toMatchSnapshot();
-});
-
-it('should correctly handle clicks on the button', () => {
- const toggle = jest.fn(() => popup.setProps({ isOpen: !popup.props().isOpen }));
- const popup = shallow(
- <BubblePopupHelper
- isOpen={false}
- offset={{ vertical: 0, horizontal: 2 }}
- position="bottomleft"
- togglePopup={toggle}
- popup={
- <BubblePopup>
- <span>test</span>
- </BubblePopup>
- }>
- <button onClick={toggle}>open</button>
- </BubblePopupHelper>,
- { disableLifecycleMethods: true }
- );
- expect(popup).toMatchSnapshot();
- click(popup.find('button'));
- expect(toggle.mock.calls.length).toBe(1);
- expect(popup).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap
deleted file mode 100644
index 08a7cb8825e..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopup-test.js.snap
+++ /dev/null
@@ -1,20 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render popup 1`] = `
-<div
- className="bubble-popup custom"
- style={
- Object {
- "right": 0,
- "top": 0,
- }
- }
->
- <span>
- test
- </span>
- <div
- className="bubble-popup-arrow"
- />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap
deleted file mode 100644
index 51f8e93fda5..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap
+++ /dev/null
@@ -1,182 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly handle clicks on the button 1`] = `
-<div
- className="bubble-popup-helper"
- onClick={[Function]}
- role="tooltip"
- tabIndex={0}
->
- <button
- onClick={[MockFunction]}
- >
- open
- </button>
-</div>
-`;
-
-exports[`should correctly handle clicks on the button 2`] = `
-<div
- className="bubble-popup-helper"
- onClick={[Function]}
- role="tooltip"
- tabIndex={0}
->
- <button
- onClick={
- [MockFunction] {
- "calls": Array [
- Array [
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
- ],
- ],
- }
- }
- >
- open
- </button>
- <div>
- <BubblePopup
- popupPosition={
- Object {
- "right": 0,
- "top": 0,
- }
- }
- >
- <span>
- test
- </span>
- </BubblePopup>
- </div>
-</div>
-`;
-
-exports[`should render an open popup on the left 1`] = `
-<BubblePopup
- popupPosition={
- Object {
- "left": 2,
- "top": 0,
- }
- }
->
- <div
- className="bubble-popup"
- style={Object {}}
- >
- <span>
- test
- </span>
- <div
- className="bubble-popup-arrow"
- />
- </div>
-</BubblePopup>
-`;
-
-exports[`should render an open popup on the right 1`] = `
-<div
- className="bubble-popup-helper"
- onClick={[Function]}
- role="tooltip"
- tabIndex={0}
->
- <button
- onClick={[MockFunction]}
- >
- open
- </button>
- <div>
- <BubblePopup
- popupPosition={
- Object {
- "right": 0,
- "top": 0,
- }
- }
- >
- <span>
- test
- </span>
- </BubblePopup>
- </div>
-</div>
-`;
-
-exports[`should render the popup helper with a closed popup 1`] = `
-<div
- className="bubble-popup-helper"
- onClick={[Function]}
- role="tooltip"
- tabIndex={0}
->
- <button
- onClick={[MockFunction]}
- >
- open
- </button>
-</div>
-`;
-
-exports[`should render the popup with offset 1`] = `
-<BubblePopup
- popupPosition={
- Object {
- "right": 2,
- "top": 5,
- }
- }
->
- <div
- className="bubble-popup"
- style={Object {}}
- >
- <span>
- test
- </span>
- <div
- className="bubble-popup-arrow"
- />
- </div>
-</BubblePopup>
-`;
-
-exports[`should render with custom classes 1`] = `
-<div
- className="bubble-popup-helper"
- onClick={[Function]}
- role="tooltip"
- tabIndex={0}
->
- <button
- onClick={[MockFunction]}
- >
- open
- </button>
- <div>
- <BubblePopup
- customClass="mypopupclass"
- popupPosition={
- Object {
- "right": 0,
- "top": 0,
- }
- }
- >
- <span>
- test
- </span>
- </BubblePopup>
- </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
index f1173c88e75..252d26ef618 100644
--- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
@@ -28,34 +28,24 @@ import { Button } from '../ui/buttons';
interface Props {
className?: string;
children: React.ReactNode;
- menuClassName?: string;
- menuPosition?: 'left' | 'right';
onOpen?: () => void;
small?: boolean;
toggleClassName?: string;
}
-export default function ActionsDropdown({ menuPosition = 'right', ...props }: Props) {
+export default function ActionsDropdown(props: Props) {
return (
- <Dropdown onOpen={props.onOpen}>
- {({ onToggleClick, open }) => (
- <div className={classNames('dropdown', props.className, { open })}>
- <Button
- className={classNames('dropdown-toggle', props.toggleClassName, {
- 'button-small': props.small
- })}
- onClick={onToggleClick}>
- <SettingsIcon className="text-text-bottom" />
- <i className="icon-dropdown little-spacer-left" />
- </Button>
- <ul
- className={classNames('dropdown-menu', props.menuClassName, {
- 'dropdown-menu-right': menuPosition === 'right'
- })}>
- {props.children}
- </ul>
- </div>
- )}
+ <Dropdown
+ className={props.className}
+ onOpen={props.onOpen}
+ overlay={<ul className="menu">{props.children}</ul>}>
+ <Button
+ className={classNames('dropdown-toggle', props.toggleClassName, {
+ 'button-small': props.small
+ })}>
+ <SettingsIcon className="text-text-bottom" />
+ <i className="icon-dropdown little-spacer-left" />
+ </Button>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx
index e5794463a32..886a43211e4 100644
--- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx
+++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx
@@ -151,73 +151,71 @@ export default class DateInput extends React.PureComponent<Props, State> {
return (
<OutsideClickHandler onClickOutside={this.closeCalendar}>
- {({ ref }) => (
- <span className={classNames('date-input-control', this.props.className)} ref={ref}>
- <input
- className={classNames('date-input-control-input', this.props.inputClassName, {
- 'is-filled': this.props.value !== undefined
- })}
- name={this.props.name}
- onFocus={this.openCalendar}
- placeholder={this.props.placeholder}
- readOnly={true}
- ref={node => (this.input = node)}
- type="text"
- value={formattedValue || ''}
- />
- <CalendarIcon className="date-input-control-icon" fill="" />
- {this.props.value !== undefined && (
- <ButtonIcon
- className="button-tiny date-input-control-reset"
- color={theme.gray60}
- onClick={this.handleResetClick}>
- <ClearIcon size={12} />
- </ButtonIcon>
- )}
- {this.state.open && (
- <div className="date-input-calendar">
- <nav className="date-input-calendar-nav">
- <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}>
- <ChevronLeftIcon />
- </ButtonIcon>
- <div className="date-input-calender-month">
- <Select
- className="date-input-calender-month-select"
- onChange={this.handleCurrentMonthChange}
- options={months.map(month => ({
- label: getShortMonthName(month),
- value: month
- }))}
- value={this.state.currentMonth.getMonth()}
- />
- <Select
- className="date-input-calender-month-select spacer-left"
- onChange={this.handleCurrentYearChange}
- options={years.map(year => ({ label: String(year), value: year }))}
- value={this.state.currentMonth.getFullYear()}
- />
- </div>
- <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}>
- <ChevronRightIcon />
- </ButtonIcon>
- </nav>
- <DayPicker
- captionElement={<NullComponent />}
- disabledDays={{ after, before: minDate }}
- firstDayOfWeek={1}
- modifiers={modifiers}
- month={this.state.currentMonth}
- navbarElement={<NullComponent />}
- onDayClick={this.handleDayClick}
- onDayMouseEnter={this.handleDayMouseEnter}
- selectedDays={selectedDays}
- weekdaysLong={weekdaysLong}
- weekdaysShort={weekdaysShort}
- />
- </div>
- )}
- </span>
- )}
+ <span className={classNames('date-input-control', this.props.className)}>
+ <input
+ className={classNames('date-input-control-input', this.props.inputClassName, {
+ 'is-filled': this.props.value !== undefined
+ })}
+ name={this.props.name}
+ onFocus={this.openCalendar}
+ placeholder={this.props.placeholder}
+ readOnly={true}
+ ref={node => (this.input = node)}
+ type="text"
+ value={formattedValue || ''}
+ />
+ <CalendarIcon className="date-input-control-icon" fill="" />
+ {this.props.value !== undefined && (
+ <ButtonIcon
+ className="button-tiny date-input-control-reset"
+ color={theme.gray60}
+ onClick={this.handleResetClick}>
+ <ClearIcon size={12} />
+ </ButtonIcon>
+ )}
+ {this.state.open && (
+ <div className="date-input-calendar">
+ <nav className="date-input-calendar-nav">
+ <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}>
+ <ChevronLeftIcon />
+ </ButtonIcon>
+ <div className="date-input-calender-month">
+ <Select
+ className="date-input-calender-month-select"
+ onChange={this.handleCurrentMonthChange}
+ options={months.map(month => ({
+ label: getShortMonthName(month),
+ value: month
+ }))}
+ value={this.state.currentMonth.getMonth()}
+ />
+ <Select
+ className="date-input-calender-month-select spacer-left"
+ onChange={this.handleCurrentYearChange}
+ options={years.map(year => ({ label: String(year), value: year }))}
+ value={this.state.currentMonth.getFullYear()}
+ />
+ </div>
+ <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}>
+ <ChevronRightIcon />
+ </ButtonIcon>
+ </nav>
+ <DayPicker
+ captionElement={<NullComponent />}
+ disabledDays={{ after, before: minDate }}
+ firstDayOfWeek={1}
+ modifiers={modifiers}
+ month={this.state.currentMonth}
+ navbarElement={<NullComponent />}
+ onDayClick={this.handleDayClick}
+ onDayMouseEnter={this.handleDayMouseEnter}
+ selectedDays={selectedDays}
+ weekdaysLong={weekdaysLong}
+ weekdaysShort={weekdaysShort}
+ />
+ </div>
+ )}
+ </span>
</OutsideClickHandler>
);
}
diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx
index e23a99b7e25..9c13423e454 100644
--- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx
+++ b/server/sonar-web/src/main/js/components/controls/DocumentClickHandler.tsx
@@ -18,32 +18,36 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
-
-export interface BubblePopupPosition {
- top?: number;
- left?: number;
- right?: number;
-}
interface Props {
- customClass?: string;
children: React.ReactNode;
- position?: BubblePopupPosition;
+ onClick: () => void;
}
-/**
- * Deprecated.
- * Use <Popup /> instead.
- */
-export default function BubblePopup(props: Props) {
- const popupClass = classNames('bubble-popup', props.customClass);
- const popupStyle = { ...props.position };
+export default class DocumentClickHandler extends React.Component<Props> {
+ componentDidMount() {
+ setTimeout(() => {
+ this.addClickHandler();
+ }, 0);
+ }
+
+ componentWillUnmount() {
+ this.removeClickHandler();
+ }
+
+ addClickHandler = () => {
+ document.addEventListener('click', this.handleDocumentClick);
+ };
+
+ removeClickHandler = () => {
+ document.removeEventListener('click', this.handleDocumentClick);
+ };
+
+ handleDocumentClick = () => {
+ this.props.onClick();
+ };
- return (
- <div className={popupClass} style={popupStyle}>
- {props.children}
- <div className="bubble-popup-arrow" />
- </div>
- );
+ render() {
+ return this.props.children;
+ }
}
diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
index 3b82fd1c408..a90414a235c 100644
--- a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
@@ -18,16 +18,33 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
+import ScreenPositionFixer from './ScreenPositionFixer';
+import Toggler from './Toggler';
+import { Popup, PopupPlacement } from '../ui/popups';
+
+interface OnClickCallback {
+ (event?: React.SyntheticEvent<HTMLElement>): void;
+}
interface RenderProps {
closeDropdown: () => void;
- onToggleClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
+ onToggleClick: OnClickCallback;
open: boolean;
}
interface Props {
- children: (renderProps: RenderProps) => JSX.Element;
+ children:
+ | ((renderProps: RenderProps) => JSX.Element)
+ | React.ReactElement<{ onClick: OnClickCallback }>;
+ className?: string;
+ closeOnClick?: boolean;
+ closeOnClickOutside?: boolean;
onOpen?: () => void;
+ overlay: React.ReactNode;
+ overlayPlacement?: PopupPlacement;
+ noOverlayPadding?: boolean;
+ tagName?: string;
}
interface State {
@@ -35,49 +52,18 @@ interface State {
}
export default class Dropdown extends React.PureComponent<Props, State> {
- toggleNode?: HTMLElement;
-
- constructor(props: Props) {
- super(props);
- this.state = { open: false };
- }
+ state: State = { open: false };
componentDidUpdate(_: Props, prevState: State) {
- if (!prevState.open && this.state.open) {
- this.addClickHandler();
- if (this.props.onOpen) {
- this.props.onOpen();
- }
- }
-
- if (prevState.open && !this.state.open) {
- this.removeClickHandler();
+ if (!prevState.open && this.state.open && this.props.onOpen) {
+ this.props.onOpen();
}
}
- componentWillUnmount() {
- this.removeClickHandler();
- }
-
- addClickHandler = () => {
- window.addEventListener('click', this.handleWindowClick);
- };
-
- removeClickHandler = () => {
- window.removeEventListener('click', this.handleWindowClick);
- };
-
- handleWindowClick = (event: MouseEvent) => {
- if (!this.toggleNode || !this.toggleNode.contains(event.target as Node)) {
- this.closeDropdown();
- }
- };
-
closeDropdown = () => this.setState({ open: false });
handleToggleClick = (event?: React.SyntheticEvent<HTMLElement>) => {
if (event) {
- this.toggleNode = event.currentTarget;
event.preventDefault();
event.currentTarget.blur();
}
@@ -85,10 +71,89 @@ export default class Dropdown extends React.PureComponent<Props, State> {
};
render() {
- return this.props.children({
- closeDropdown: this.closeDropdown,
- onToggleClick: this.handleToggleClick,
- open: this.state.open
- });
+ const a11yAttrs = {
+ 'aria-expanded': String(this.state.open),
+ 'aria-haspopup': 'true'
+ };
+
+ const child = React.isValidElement(this.props.children)
+ ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs })
+ : this.props.children({
+ closeDropdown: this.closeDropdown,
+ onToggleClick: this.handleToggleClick,
+ open: this.state.open
+ });
+
+ const { closeOnClick = true, closeOnClickOutside = false } = this.props;
+
+ const toggler = (
+ <Toggler
+ closeOnClick={closeOnClick}
+ closeOnClickOutside={closeOnClickOutside}
+ onRequestClose={this.closeDropdown}
+ open={this.state.open}
+ overlay={
+ <DropdownOverlay
+ noPadding={this.props.noOverlayPadding}
+ placement={this.props.overlayPlacement}>
+ {this.props.overlay}
+ </DropdownOverlay>
+ }>
+ {child}
+ </Toggler>
+ );
+
+ return React.createElement(
+ this.props.tagName || 'div',
+ { className: classNames('dropdown', this.props.className) },
+ toggler
+ );
+ }
+}
+
+interface OverlayProps {
+ className?: string;
+ children: React.ReactNode;
+ noPadding?: boolean;
+ placement?: PopupPlacement;
+}
+
+// TODO use the same styling for <Select />
+// TODO use the same styling for <DateInput />
+
+export class DropdownOverlay extends React.Component<OverlayProps> {
+ get placement() {
+ return this.props.placement || PopupPlacement.Bottom;
+ }
+
+ renderPopup = (leftFix?: number, topFix?: number) => (
+ <Popup
+ arrowStyle={
+ leftFix !== undefined && topFix !== undefined
+ ? { transform: `translate(${-leftFix}px, ${-topFix}px)` }
+ : undefined
+ }
+ className={this.props.className}
+ noPadding={this.props.noPadding}
+ placement={this.placement}
+ style={
+ leftFix !== undefined && topFix !== undefined
+ ? { marginLeft: `calc(50% + ${leftFix}px)` }
+ : undefined
+ }>
+ {this.props.children}
+ </Popup>
+ );
+
+ render() {
+ if (this.placement === PopupPlacement.Bottom) {
+ return (
+ <ScreenPositionFixer>
+ {({ leftFix, topFix }) => this.renderPopup(leftFix, topFix)}
+ </ScreenPositionFixer>
+ );
+ } else {
+ return this.renderPopup();
+ }
}
}
diff --git a/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx b/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx
index 3886a89e19f..f5e5d6ec587 100644
--- a/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx
+++ b/server/sonar-web/src/main/js/components/controls/OutsideClickHandler.tsx
@@ -18,9 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { findDOMNode } from 'react-dom';
interface Props {
- children: (props: { ref: React.Ref<Element> }) => React.ReactNode;
+ children: React.ReactNode;
onClickOutside: () => void;
}
@@ -28,7 +29,9 @@ export default class OutsideClickHandler extends React.Component<Props> {
element?: Element | null;
componentDidMount() {
- this.addClickHandler();
+ setTimeout(() => {
+ this.addClickHandler();
+ }, 0);
}
componentWillUnmount() {
@@ -44,16 +47,14 @@ export default class OutsideClickHandler extends React.Component<Props> {
};
handleWindowClick = (event: MouseEvent) => {
- if (!this.element || !this.element.contains(event.target as Node)) {
+ // eslint-disable-next-line react/no-find-dom-node
+ const node = findDOMNode(this);
+ if (!node || !node.contains(event.target as Node)) {
this.props.onClickOutside();
}
};
- handleRef = (element: Element | null) => {
- this.element = element;
- };
-
render() {
- return this.props.children({ ref: this.handleRef });
+ return this.props.children;
}
}
diff --git a/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx b/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx
new file mode 100644
index 00000000000..ebf28cf924a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { findDOMNode } from 'react-dom';
+import { throttle } from 'lodash';
+import { grid } from '../../app/theme';
+
+const EDGE_MARGIN = 0.5 * grid;
+
+interface Props {
+ /**
+ * First time `children` are rendered with `undefined` fixes to measure the offset.
+ * Second time it renders with the computed fixes.
+ */
+ children: (props: Fixes) => React.ReactNode;
+
+ /**
+ * Use this flag to force re-positioning.
+ * Use cases:
+ * - when you need to measure `children` size first
+ * - when you load content asynchronously
+ */
+ ready?: boolean;
+}
+
+interface Fixes {
+ leftFix?: number;
+ topFix?: number;
+}
+
+export default class ScreenPositionFixer extends React.Component<Props, Fixes> {
+ throttledPosition: () => void;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {};
+ this.throttledPosition = throttle(this.position, 50);
+ }
+
+ componentDidMount() {
+ this.addEventListeners();
+ this.position();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!prevProps.ready && this.props.ready) {
+ this.position();
+ }
+ }
+
+ componentWillUnmount() {
+ this.removeEventListeners();
+ }
+
+ addEventListeners = () => {
+ window.addEventListener('resize', this.throttledPosition);
+ };
+
+ removeEventListeners = () => {
+ window.removeEventListener('resize', this.throttledPosition);
+ };
+
+ position = () => {
+ // eslint-disable-next-line react/no-find-dom-node
+ const node = findDOMNode(this);
+
+ const { width, height, left, top } = node.getBoundingClientRect();
+
+ const { clientHeight, clientWidth } = document.body;
+
+ let leftFix = 0;
+ if (left < EDGE_MARGIN) {
+ leftFix = EDGE_MARGIN - left;
+ } else if (left + width > clientWidth - EDGE_MARGIN) {
+ leftFix = clientWidth - EDGE_MARGIN - left - width;
+ }
+
+ let topFix = 0;
+ if (top < EDGE_MARGIN) {
+ topFix = EDGE_MARGIN - top;
+ } else if (top + height > clientHeight - EDGE_MARGIN) {
+ topFix = clientHeight - EDGE_MARGIN - top - height;
+ }
+
+ this.setState({ leftFix, topFix });
+ };
+
+ render() {
+ return this.props.children(this.state);
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/Toggler.tsx b/server/sonar-web/src/main/js/components/controls/Toggler.tsx
new file mode 100644
index 00000000000..37bb43bf597
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/Toggler.tsx
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DocumentClickHandler from './DocumentClickHandler';
+import OutsideClickHandler from './OutsideClickHandler';
+
+export interface Props {
+ children?: React.ReactNode;
+ closeOnClick?: boolean;
+ closeOnClickOutside?: boolean;
+ closeOnEscape?: boolean;
+ onRequestClose: () => void;
+ open: boolean;
+ overlay: React.ReactNode;
+}
+
+export default class Toggler extends React.Component<Props> {
+ componentDidMount() {
+ if (this.props.open && isTrueOrUndefined(this.props.closeOnEscape)) {
+ this.addEventListeners();
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!prevProps.open && this.props.open && isTrueOrUndefined(this.props.closeOnEscape)) {
+ this.addEventListeners();
+ } else if (prevProps.open && !this.props.open) {
+ this.removeEventListeners();
+ } else if (
+ isTrueOrUndefined(prevProps.closeOnEscape) &&
+ !isTrueOrUndefined(this.props.closeOnEscape)
+ ) {
+ this.removeEventListeners();
+ }
+ }
+
+ componentWillUnmount() {
+ this.removeEventListeners();
+ }
+
+ addEventListeners() {
+ document.addEventListener('keydown', this.handleKeyDown, false);
+ }
+
+ removeEventListeners() {
+ document.removeEventListener('keydown', this.handleKeyDown, false);
+ }
+
+ handleKeyDown = (event: KeyboardEvent) => {
+ // Escape key
+ if (event.keyCode === 27) {
+ this.props.onRequestClose();
+ }
+ };
+
+ renderOverlay() {
+ const {
+ closeOnClick = false,
+ closeOnClickOutside = true,
+ onRequestClose,
+ overlay
+ } = this.props;
+
+ if (closeOnClick) {
+ return <DocumentClickHandler onClick={onRequestClose}>{overlay}</DocumentClickHandler>;
+ } else if (closeOnClickOutside) {
+ return <OutsideClickHandler onClickOutside={onRequestClose}>{overlay}</OutsideClickHandler>;
+ } else {
+ return overlay;
+ }
+ }
+
+ render() {
+ return (
+ <>
+ {this.props.children}
+ {this.props.open && this.renderOverlay()}
+ </>
+ );
+ }
+}
+
+function isTrueOrUndefined(x: boolean | undefined) {
+ return x === true || x === undefined;
+}
diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
index ca6803887e9..14dfa11da66 100644
--- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { createPortal, findDOMNode } from 'react-dom';
import { throttle } from 'lodash';
+import ScreePositionFixer from './ScreenPositionFixer';
import './Tooltip.css';
export type Placement = 'bottom' | 'right' | 'left' | 'top';
@@ -39,9 +40,7 @@ interface Props {
interface Measurements {
height: number;
left: number;
- leftFix: number;
top: number;
- topFix: number;
width: number;
}
@@ -55,8 +54,6 @@ function isMeasured(state: State): state is OwnState & Measurements {
return state.height !== undefined;
}
-const EDGE_MARGIN = 4;
-
export default function Tooltip(props: Props) {
// allows to pass `undefined` to `overlay` to avoid rendering a tooltip
// can useful in some cases to render the tooltip conditionally
@@ -182,31 +179,11 @@ export class TooltipInner extends React.Component<Props, State> {
break;
}
- // make sure the tooltip fits in the document
- // it may go out of the current viewport, if it has scrolls
- const { scrollWidth, scrollHeight } = document.documentElement;
-
- let leftFix = 0;
- if (left < EDGE_MARGIN) {
- leftFix = EDGE_MARGIN - left;
- } else if (left + width > scrollWidth - EDGE_MARGIN) {
- leftFix = scrollWidth - EDGE_MARGIN - left - width;
- }
-
- let topFix = 0;
- if (top < EDGE_MARGIN) {
- topFix = EDGE_MARGIN - top;
- } else if (top + height > scrollHeight - EDGE_MARGIN) {
- topFix = scrollHeight - EDGE_MARGIN - top - height;
- }
-
// save width and height (and later set in `render`) to avoid resizing the tooltip element,
// when it's placed close to the window edge
const measurements: Measurements = {
left: window.pageXOffset + left,
- leftFix,
top: window.pageYOffset + top,
- topFix,
width,
height
};
@@ -217,9 +194,7 @@ export class TooltipInner extends React.Component<Props, State> {
clearPosition = () => {
this.setState({
left: undefined,
- leftFix: undefined,
top: undefined,
- topFix: undefined,
width: undefined,
height: undefined
});
@@ -280,31 +255,35 @@ export class TooltipInner extends React.Component<Props, State> {
})}
{this.isVisible() && (
<TooltipPortal>
- <div
- className={`${classNameSpace} ${this.getPlacement()}`}
- onMouseEnter={this.handleOverlayMouseEnter}
- onMouseLeave={this.handleOverlayMouseLeave}
- ref={this.tooltipNodeRef}
- style={
- isMeasured(this.state)
- ? {
- left: this.state.left + this.state.leftFix,
- top: this.state.top + this.state.topFix,
- width: this.state.width,
- height: this.state.height
+ <ScreePositionFixer ready={isMeasured(this.state)}>
+ {({ leftFix = 0, topFix = 0 }) => (
+ <div
+ className={`${classNameSpace} ${this.getPlacement()}`}
+ onMouseEnter={this.handleOverlayMouseEnter}
+ onMouseLeave={this.handleOverlayMouseLeave}
+ ref={this.tooltipNodeRef}
+ style={
+ isMeasured(this.state)
+ ? {
+ left: this.state.left + leftFix,
+ top: this.state.top + topFix,
+ width: this.state.width,
+ height: this.state.height
+ }
+ : undefined
+ }>
+ <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div>
+ <div
+ className={`${classNameSpace}-arrow`}
+ style={
+ isMeasured(this.state)
+ ? { marginLeft: -leftFix, marginTop: -topFix }
+ : undefined
}
- : undefined
- }>
- <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div>
- <div
- className={`${classNameSpace}-arrow`}
- style={
- isMeasured(this.state)
- ? { marginLeft: -this.state.leftFix, marginTop: -this.state.topFix }
- : undefined
- }
- />
- </div>
+ />
+ </div>
+ )}
+ </ScreePositionFixer>
</TooltipPortal>
)}
</>
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
index 37c974480ec..564f884d920 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
@@ -39,13 +39,12 @@ it('should render', () => {
const { wrapper } = shallowRender();
expect(wrapper).toMatchSnapshot();
- expect(wrapper.dive()).toMatchSnapshot();
wrapper.setProps({ value: dateA });
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
wrapper.setState({ open: true });
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
});
it('should change current month', () => {
@@ -76,8 +75,9 @@ it('should select a day', () => {
instance.handleDayClick(dateA, { outside: undefined, today: undefined });
expect(onChange).lastCalledWith(dateA);
+ wrapper.update();
expect(wrapper.state().open).toBe(false);
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
instance.handleResetClick();
expect(onChange).lastCalledWith(undefined);
@@ -90,7 +90,7 @@ it('should hightlightFrom range', () => {
const dateC = addDays(dateA, 3);
instance.handleDayMouseEnter(dateC, { outside: undefined, today: undefined });
wrapper.update();
- const dayPicker = wrapper.dive().find('DayPicker');
+ const dayPicker = wrapper.find('DayPicker');
expect(dayPicker.prop('modifiers')).toEqual({ highlighted: { from: dateA, to: dateC } });
expect(dayPicker.prop('selectedDays')).toEqual([dateA]);
});
@@ -102,7 +102,7 @@ it('should hightlightTo range', () => {
const dateC = subDays(dateB, 5);
instance.handleDayMouseEnter(dateC, { outside: undefined, today: undefined });
wrapper.update();
- const dayPicker = wrapper.dive().find('DayPicker');
+ const dayPicker = wrapper.find('DayPicker');
expect(dayPicker.prop('modifiers')).toEqual({ highlighted: { from: dateC, to: dateB } });
expect(dayPicker.prop('selectedDays')).toEqual([dateB]);
});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx
index 096dc8356ba..52df4675a0b 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx
@@ -18,28 +18,92 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { shallow } from 'enzyme';
-import Dropdown from '../Dropdown';
+import { shallow, mount, ShallowWrapper } from 'enzyme';
+import Dropdown, { DropdownOverlay } from '../Dropdown';
import { Button } from '../../ui/buttons';
import { click } from '../../../helpers/testUtils';
+import { PopupPlacement } from '../../ui/popups';
-it('renders', () => {
- expect(
- shallow(<Dropdown>{() => <div />}</Dropdown>)
- .find('div')
- .exists()
- ).toBeTruthy();
-});
+describe('Dropdown', () => {
+ it('renders', () => {
+ expect(
+ shallow(<Dropdown overlay={<div id="overlay" />}>{() => <div />}</Dropdown>)
+ .find('div')
+ .exists()
+ ).toBeTruthy();
+ });
+
+ it('toggles with element child', () => {
+ checkToggle(
+ shallow(
+ <Dropdown overlay={<div id="overlay" />}>
+ <Button />
+ </Dropdown>
+ )
+ );
+
+ checkToggle(
+ shallow(
+ <Dropdown overlay={<div id="overlay" />}>
+ <a href="#">click me!</a>
+ </Dropdown>
+ ),
+ 'a'
+ );
+ });
+
+ it('toggles with render prop', () => {
+ checkToggle(
+ shallow(
+ <Dropdown overlay={<div id="overlay" />}>
+ {({ onToggleClick }) => <Button onClick={onToggleClick} />}
+ </Dropdown>
+ )
+ );
+ });
-it('toggles', () => {
- const wrapper = shallow(
- <Dropdown>{({ onToggleClick }) => <Button onClick={onToggleClick} />}</Dropdown>
- );
- expect(wrapper.state()).toEqual({ open: false });
+ it('should call onOpen', () => {
+ const onOpen = jest.fn();
+ const wrapper = mount(
+ <Dropdown onOpen={onOpen} overlay={<div id="overlay" />}>
+ <Button />
+ </Dropdown>
+ );
+ expect(onOpen).not.toBeCalled();
+ click(wrapper.find('Button'));
+ expect(onOpen).toBeCalled();
+ });
+
+ function checkToggle(wrapper: ShallowWrapper, selector = 'Button') {
+ expect(wrapper.state()).toEqual({ open: false });
+
+ click(wrapper.find(selector));
+ expect(wrapper.state()).toEqual({ open: true });
+
+ click(wrapper.find(selector));
+ expect(wrapper.state()).toEqual({ open: false });
+ }
+});
- click(wrapper.find('Button'));
- expect(wrapper.state()).toEqual({ open: true });
+describe('DropdownOverlay', () => {
+ it('should render overlay with screen fixer', () => {
+ const wrapper = shallow(
+ <DropdownOverlay>
+ <div />
+ </DropdownOverlay>,
+ // disable ScreenPositionFixer positioning
+ { disableLifecycleMethods: true }
+ );
+ expect(wrapper.is('ScreenPositionFixer')).toBe(true);
+ expect(wrapper.dive().is('Popup')).toBe(true);
+ });
- click(wrapper.find('Button'));
- expect(wrapper.state()).toEqual({ open: false });
+ it('should render overlay without screen fixer', () => {
+ const wrapper = shallow(
+ <DropdownOverlay placement={PopupPlacement.BottomRight}>
+ <div />
+ </DropdownOverlay>
+ );
+ expect(wrapper.is('Popup')).toBe(true);
+ });
});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx
new file mode 100644
index 00000000000..9e389326629
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/ScreenPositionFixer-test.tsx
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { mount } from 'enzyme';
+import ScreenPositionFixer from '../ScreenPositionFixer';
+import { resizeWindowTo } from '../../../helpers/testUtils';
+
+jest.mock('lodash', () => {
+ const lodash = require.requireActual('lodash');
+ lodash.throttle = (fn: any) => () => fn();
+ return lodash;
+});
+
+jest.mock('react-dom', () => ({
+ findDOMNode: jest.fn(() => ({
+ getBoundingClientRect: () => ({ width: 0, height: 0, left: 0, top: 0 })
+ }))
+}));
+
+beforeEach(() => {
+ setNodeRect({ width: 50, height: 50, left: 50, top: 50 });
+ resizeWindowTo(1000, 1000);
+});
+
+it('should fix position', () => {
+ const renderer = jest.fn(() => <div />);
+ mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>);
+
+ setNodeRect({ width: 50, height: 50, left: 50, top: 50 });
+ resizeWindowTo(75, 1000);
+ expect(renderer).toHaveBeenLastCalledWith({ leftFix: -29, topFix: 0 });
+
+ resizeWindowTo(1000, 75);
+ expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: -29 });
+
+ setNodeRect({ width: 50, height: 50, left: -10, top: 50 });
+ resizeWindowTo(1000, 1000);
+ expect(renderer).toHaveBeenLastCalledWith({ leftFix: 14, topFix: 0 });
+
+ setNodeRect({ width: 50, height: 50, left: 50, top: -10 });
+ resizeWindowTo();
+ expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 14 });
+});
+
+it('should render two times', () => {
+ const renderer = jest.fn(() => <div />);
+ mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>);
+ expect(renderer).toHaveBeenCalledTimes(2);
+ expect(renderer).toHaveBeenCalledWith({});
+ expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 0 });
+});
+
+it('should re-position when `ready` turns to `true`', () => {
+ const renderer = jest.fn(() => <div />);
+ const wrapper = mount(<ScreenPositionFixer ready={false}>{renderer}</ScreenPositionFixer>);
+ expect(renderer).toHaveBeenCalledTimes(2);
+ wrapper.setProps({ ready: true });
+ // 2 + 1 (props change) + 1 (new measurement)
+ expect(renderer).toHaveBeenCalledTimes(4);
+});
+
+it('should re-position when window is resized', () => {
+ const renderer = jest.fn(() => <div />);
+ const wrapper = mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>);
+ expect(renderer).toHaveBeenCalledTimes(2);
+
+ resizeWindowTo();
+ // 2 + 1 (new measurement)
+ expect(renderer).toHaveBeenCalledTimes(3);
+
+ wrapper.unmount();
+ resizeWindowTo();
+ expect(renderer).toHaveBeenCalledTimes(3);
+});
+
+function setNodeRect(rect: { width: number; height: number; left: number; top: number }) {
+ const findDOMNode = require('react-dom').findDOMNode as jest.Mock<any>;
+ findDOMNode.mockImplementation(() => ({
+ getBoundingClientRect: () => rect
+ }));
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx
new file mode 100644
index 00000000000..ee087ecbc6c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Toggler, { Props } from '../Toggler';
+import { keydown } from '../../../helpers/testUtils';
+
+it('should render only children', () => {
+ expect(shallowRender({ open: false })).toMatchSnapshot();
+});
+
+it('should render children and overlay', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render when closeOnClick=true', () => {
+ expect(shallowRender({ closeOnClick: true })).toMatchSnapshot();
+});
+
+it('should not render click wrappers', () => {
+ expect(shallowRender({ closeOnClick: false, closeOnClickOutside: false })).toMatchSnapshot();
+});
+
+it('should close on escape', () => {
+ const onRequestClose = jest.fn();
+ const wrapper = shallowRender({
+ closeOnClick: false,
+ closeOnClickOutside: false,
+ onRequestClose
+ });
+ keydown(27);
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ closeOnEscape: false });
+ keydown(27);
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ open: false });
+ wrapper.setProps({ closeOnEscape: true, open: true });
+ keydown(27);
+ expect(onRequestClose).toHaveBeenCalledTimes(2);
+
+ wrapper.unmount();
+ keydown(27);
+ expect(onRequestClose).toHaveBeenCalledTimes(2);
+});
+
+function shallowRender(props?: Partial<Props>) {
+ return shallow(
+ <Toggler onRequestClose={jest.fn()} open={true} overlay={<div id="overlay" />} {...props}>
+ <div id="toggle" />
+ </Toggler>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
index af6836077c7..5485b05984e 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
@@ -3,274 +3,284 @@
exports[`should render 1`] = `
<OutsideClickHandler
onClickOutside={[Function]}
-/>
-`;
-
-exports[`should render 2`] = `
-<span
- className="date-input-control"
>
- <input
- className="date-input-control-input"
- onFocus={[Function]}
- placeholder="placeholder"
- readOnly={true}
- type="text"
- value=""
- />
- <CalendarIcon
- className="date-input-control-icon"
- fill=""
- />
-</span>
+ <span
+ className="date-input-control"
+ >
+ <input
+ className="date-input-control-input"
+ onFocus={[Function]}
+ placeholder="placeholder"
+ readOnly={true}
+ type="text"
+ value=""
+ />
+ <CalendarIcon
+ className="date-input-control-icon"
+ fill=""
+ />
+ </span>
+</OutsideClickHandler>
`;
-exports[`should render 3`] = `
-<span
- className="date-input-control"
+exports[`should render 2`] = `
+<OutsideClickHandler
+ onClickOutside={[Function]}
>
- <input
- className="date-input-control-input is-filled"
- onFocus={[Function]}
- placeholder="placeholder"
- readOnly={true}
- type="text"
- value="Jan 17, 2018"
- />
- <CalendarIcon
- className="date-input-control-icon"
- fill=""
- />
- <ButtonIcon
- className="button-tiny date-input-control-reset"
- color="#999"
- onClick={[Function]}
+ <span
+ className="date-input-control"
>
- <ClearIcon
- size={12}
+ <input
+ className="date-input-control-input is-filled"
+ onFocus={[Function]}
+ placeholder="placeholder"
+ readOnly={true}
+ type="text"
+ value="Jan 17, 2018"
/>
- </ButtonIcon>
-</span>
+ <CalendarIcon
+ className="date-input-control-icon"
+ fill=""
+ />
+ <ButtonIcon
+ className="button-tiny date-input-control-reset"
+ color="#999"
+ onClick={[Function]}
+ >
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+ </span>
+</OutsideClickHandler>
`;
-exports[`should render 4`] = `
-<span
- className="date-input-control"
+exports[`should render 3`] = `
+<OutsideClickHandler
+ onClickOutside={[Function]}
>
- <input
- className="date-input-control-input is-filled"
- onFocus={[Function]}
- placeholder="placeholder"
- readOnly={true}
- type="text"
- value="Jan 17, 2018"
- />
- <CalendarIcon
- className="date-input-control-icon"
- fill=""
- />
- <ButtonIcon
- className="button-tiny date-input-control-reset"
- color="#999"
- onClick={[Function]}
+ <span
+ className="date-input-control"
>
- <ClearIcon
- size={12}
+ <input
+ className="date-input-control-input is-filled"
+ onFocus={[Function]}
+ placeholder="placeholder"
+ readOnly={true}
+ type="text"
+ value="Jan 17, 2018"
/>
- </ButtonIcon>
- <div
- className="date-input-calendar"
- >
- <nav
- className="date-input-calendar-nav"
+ <CalendarIcon
+ className="date-input-control-icon"
+ fill=""
+ />
+ <ButtonIcon
+ className="button-tiny date-input-control-reset"
+ color="#999"
+ onClick={[Function]}
>
- <ButtonIcon
- className="button-small"
- onClick={[Function]}
- >
- <ChevronLeftIcon />
- </ButtonIcon>
- <div
- className="date-input-calender-month"
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+ <div
+ className="date-input-calendar"
+ >
+ <nav
+ className="date-input-calendar-nav"
>
- <Select
- className="date-input-calender-month-select"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "Jan",
- "value": 0,
- },
- Object {
- "label": "Feb",
- "value": 1,
- },
- Object {
- "label": "Mar",
- "value": 2,
- },
- Object {
- "label": "Apr",
- "value": 3,
- },
- Object {
- "label": "May",
- "value": 4,
- },
- Object {
- "label": "Jun",
- "value": 5,
- },
- Object {
- "label": "Jul",
- "value": 6,
- },
- Object {
- "label": "Aug",
- "value": 7,
- },
- Object {
- "label": "Sep",
- "value": 8,
- },
- Object {
- "label": "Oct",
- "value": 9,
- },
- Object {
- "label": "Nov",
- "value": 10,
- },
- Object {
- "label": "Dec",
- "value": 11,
- },
- ]
+ <ButtonIcon
+ className="button-small"
+ onClick={[Function]}
+ >
+ <ChevronLeftIcon />
+ </ButtonIcon>
+ <div
+ className="date-input-calender-month"
+ >
+ <Select
+ className="date-input-calender-month-select"
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Jan",
+ "value": 0,
+ },
+ Object {
+ "label": "Feb",
+ "value": 1,
+ },
+ Object {
+ "label": "Mar",
+ "value": 2,
+ },
+ Object {
+ "label": "Apr",
+ "value": 3,
+ },
+ Object {
+ "label": "May",
+ "value": 4,
+ },
+ Object {
+ "label": "Jun",
+ "value": 5,
+ },
+ Object {
+ "label": "Jul",
+ "value": 6,
+ },
+ Object {
+ "label": "Aug",
+ "value": 7,
+ },
+ Object {
+ "label": "Sep",
+ "value": 8,
+ },
+ Object {
+ "label": "Oct",
+ "value": 9,
+ },
+ Object {
+ "label": "Nov",
+ "value": 10,
+ },
+ Object {
+ "label": "Dec",
+ "value": 11,
+ },
+ ]
+ }
+ value={0}
+ />
+ <Select
+ className="date-input-calender-month-select spacer-left"
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "2008",
+ "value": 2008,
+ },
+ Object {
+ "label": "2009",
+ "value": 2009,
+ },
+ Object {
+ "label": "2010",
+ "value": 2010,
+ },
+ Object {
+ "label": "2011",
+ "value": 2011,
+ },
+ Object {
+ "label": "2012",
+ "value": 2012,
+ },
+ Object {
+ "label": "2013",
+ "value": 2013,
+ },
+ Object {
+ "label": "2014",
+ "value": 2014,
+ },
+ Object {
+ "label": "2015",
+ "value": 2015,
+ },
+ Object {
+ "label": "2016",
+ "value": 2016,
+ },
+ Object {
+ "label": "2017",
+ "value": 2017,
+ },
+ Object {
+ "label": "2018",
+ "value": 2018,
+ },
+ ]
+ }
+ value={2018}
+ />
+ </div>
+ <ButtonIcon
+ className="button-small"
+ onClick={[Function]}
+ >
+ <ChevronRightIcon />
+ </ButtonIcon>
+ </nav>
+ <DayPicker
+ captionElement={<NullComponent />}
+ disabledDays={
+ Object {
+ "after": 2018-02-05T00:00:00.000Z,
+ "before": 2018-01-17T00:00:00.000Z,
}
- value={0}
- />
- <Select
- className="date-input-calender-month-select spacer-left"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "2008",
- "value": 2008,
- },
- Object {
- "label": "2009",
- "value": 2009,
- },
- Object {
- "label": "2010",
- "value": 2010,
- },
- Object {
- "label": "2011",
- "value": 2011,
- },
- Object {
- "label": "2012",
- "value": 2012,
- },
- Object {
- "label": "2013",
- "value": 2013,
- },
- Object {
- "label": "2014",
- "value": 2014,
- },
- Object {
- "label": "2015",
- "value": 2015,
- },
- Object {
- "label": "2016",
- "value": 2016,
- },
- Object {
- "label": "2017",
- "value": 2017,
- },
- Object {
- "label": "2018",
- "value": 2018,
- },
- ]
- }
- value={2018}
- />
- </div>
- <ButtonIcon
- className="button-small"
- onClick={[Function]}
- >
- <ChevronRightIcon />
- </ButtonIcon>
- </nav>
- <DayPicker
- captionElement={<NullComponent />}
- disabledDays={
- Object {
- "after": 2018-02-05T00:00:00.000Z,
- "before": 2018-01-17T00:00:00.000Z,
}
- }
- firstDayOfWeek={1}
- month={2018-01-17T00:00:00.000Z}
- navbarElement={<NullComponent />}
- onDayClick={[Function]}
- onDayMouseEnter={[Function]}
- selectedDays={
- Array [
- 2018-01-17T00:00:00.000Z,
- ]
- }
- weekdaysLong={
- Array [
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- "Saturday",
- ]
- }
- weekdaysShort={
- Array [
- "Sun",
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- ]
- }
- />
- </div>
-</span>
+ firstDayOfWeek={1}
+ month={2018-01-17T00:00:00.000Z}
+ navbarElement={<NullComponent />}
+ onDayClick={[Function]}
+ onDayMouseEnter={[Function]}
+ selectedDays={
+ Array [
+ 2018-01-17T00:00:00.000Z,
+ ]
+ }
+ weekdaysLong={
+ Array [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ ]
+ }
+ weekdaysShort={
+ Array [
+ "Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ ]
+ }
+ />
+ </div>
+ </span>
+</OutsideClickHandler>
`;
exports[`should select a day 1`] = `
-<span
- className="date-input-control"
+<OutsideClickHandler
+ onClickOutside={[Function]}
>
- <input
- className="date-input-control-input"
- onFocus={[Function]}
- placeholder="placeholder"
- readOnly={true}
- type="text"
- value=""
- />
- <CalendarIcon
- className="date-input-control-icon"
- fill=""
- />
-</span>
+ <span
+ className="date-input-control"
+ >
+ <input
+ className="date-input-control-input"
+ onFocus={[Function]}
+ placeholder="placeholder"
+ readOnly={true}
+ type="text"
+ value=""
+ />
+ <CalendarIcon
+ className="date-input-control-icon"
+ fill=""
+ />
+ </span>
+</OutsideClickHandler>
`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap
new file mode 100644
index 00000000000..a782ff8e6f5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render click wrappers 1`] = `
+<React.Fragment>
+ <div
+ id="toggle"
+ />
+ <div
+ id="overlay"
+ />
+</React.Fragment>
+`;
+
+exports[`should render children and overlay 1`] = `
+<React.Fragment>
+ <div
+ id="toggle"
+ />
+ <OutsideClickHandler
+ onClickOutside={[MockFunction]}
+ >
+ <div
+ id="overlay"
+ />
+ </OutsideClickHandler>
+</React.Fragment>
+`;
+
+exports[`should render only children 1`] = `
+<React.Fragment>
+ <div
+ id="toggle"
+ />
+</React.Fragment>
+`;
+
+exports[`should render when closeOnClick=true 1`] = `
+<React.Fragment>
+ <div
+ id="toggle"
+ />
+ <DocumentClickHandler
+ onClick={[MockFunction]}
+ >
+ <div
+ id="overlay"
+ />
+ </DocumentClickHandler>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
index 4a369aceaa4..4127f6e586e 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
@@ -8,22 +8,9 @@ exports[`should open & close 1`] = `
onMouseLeave={[Function]}
/>
<TooltipPortal>
- <div
- className="tooltip bottom"
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- >
- <div
- className="tooltip-inner"
- >
- <span
- id="overlay"
- />
- </div>
- <div
- className="tooltip-arrow"
- />
- </div>
+ <ScreenPositionFixer
+ ready={false}
+ />
</TooltipPortal>
</React.Fragment>
`;
@@ -56,22 +43,9 @@ exports[`should render 2`] = `
onMouseLeave={[Function]}
/>
<TooltipPortal>
- <div
- className="tooltip bottom"
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- >
- <div
- className="tooltip-inner"
- >
- <span
- id="overlay"
- />
- </div>
- <div
- className="tooltip-arrow"
- />
- </div>
+ <ScreenPositionFixer
+ ready={false}
+ />
</TooltipPortal>
</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js
index 5f814a5347e..0598c4f9f07 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueView.js
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.js
@@ -51,15 +51,14 @@ export default class IssueView extends React.PureComponent {
handleCheck = (event /*: Event */) => {
event.preventDefault();
- event.stopPropagation();
if (this.props.onCheck) {
this.props.onCheck(this.props.issue.key, event);
}
};
handleClick = (event /*: Event & { target: HTMLElement } */) => {
- event.preventDefault();
- if (this.props.onClick) {
+ if (!isClickable(event.target) && this.props.onClick) {
+ event.preventDefault();
this.props.onClick(this.props.issue.key);
}
};
@@ -100,12 +99,12 @@ export default class IssueView extends React.PureComponent {
togglePopup={this.props.togglePopup}
/>
<IssueActionsBar
- issue={issue}
currentPopup={this.props.currentPopup}
+ issue={issue}
onAssign={this.props.onAssign}
+ onChange={this.props.onChange}
onFail={this.props.onFail}
togglePopup={this.props.togglePopup}
- onChange={this.props.onChange}
/>
{issue.comments &&
issue.comments.length > 0 && (
@@ -114,8 +113,8 @@ export default class IssueView extends React.PureComponent {
<IssueCommentLine
comment={comment}
key={comment.key}
- onEdit={this.editComment}
onDelete={this.deleteComment}
+ onEdit={this.editComment}
/>
))}
</div>
@@ -137,3 +136,12 @@ export default class IssueView extends React.PureComponent {
);
}
}
+
+function isClickable(node /*: any */) {
+ if (!node) {
+ return false;
+ }
+ const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA'];
+ const tagName = (node.tagName || '').toUpperCase();
+ return clickableTags.includes(tagName) || isClickable(node.parentNode);
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
index 3f7d5e84ac7..783fdec65b0 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
@@ -19,9 +19,11 @@
*/
// @flow
import React from 'react';
-import Avatar from '../../../components/ui/Avatar';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetAssigneePopup from '../popups/SetAssigneePopup';
+import Avatar from '../../../components/ui/Avatar';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
/*:: import type { Issue } from '../types'; */
@@ -43,6 +45,10 @@ export default class IssueAssign extends React.PureComponent {
this.props.togglePopup('assign', open);
};
+ handleClose = () => {
+ this.toggleAssign(false);
+ };
+
renderAssignee() {
const { issue } = this.props;
return (
@@ -67,24 +73,26 @@ export default class IssueAssign extends React.PureComponent {
render() {
if (this.props.canAssign) {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen && this.props.canAssign}
- position="bottomleft"
- togglePopup={this.toggleAssign}
- popup={
- <SetAssigneePopup
- issue={this.props.issue}
- onFail={this.props.onFail}
- onSelect={this.props.onAssign}
- />
- }>
- <button
- className="button-link issue-action issue-action-with-options js-issue-assign"
- onClick={this.toggleAssign}>
- {this.renderAssignee()}
- <i className="little-spacer-left icon-dropdown" />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ closeOnEscape={true}
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen && this.props.canAssign}
+ overlay={
+ <SetAssigneePopup
+ issue={this.props.issue}
+ onFail={this.props.onFail}
+ onSelect={this.props.onAssign}
+ />
+ }>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-assign"
+ onClick={this.toggleAssign}>
+ {this.renderAssignee()}
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+ </Toggler>
+ </div>
);
} else {
return this.renderAssignee();
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
index 43f5adc2d91..a70f6ca5a33 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
@@ -19,11 +19,12 @@
*/
// @flow
import React from 'react';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import ChangelogPopup from '../popups/ChangelogPopup';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import Toggler from '../../../components/controls/Toggler';
import Tooltip from '../../../components/controls/Tooltip';
+import { Button } from '../../../components/ui/buttons';
/*:: import type { Issue } from '../types'; */
/*::
@@ -39,35 +40,39 @@ type Props = {
export default class IssueChangelog extends React.PureComponent {
/*:: props: Props; */
- handleClick = (evt /*: SyntheticInputEvent */) => {
- evt.preventDefault();
+ toggleChangelog = (open /*: boolean | void */) => {
+ this.props.togglePopup('changelog', open);
+ };
+
+ handleClick = () => {
this.toggleChangelog();
};
- toggleChangelog = (open /*: boolean | void */) => {
- this.props.togglePopup('changelog', open);
+ handleClose = () => {
+ this.toggleChangelog(false);
};
render() {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen}
- position="bottomright"
- togglePopup={this.toggleChangelog}
- popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}>
- <Tooltip
- mouseEnterDelay={0.5}
- overlay={<DateTimeFormatter date={this.props.creationDate} />}>
- <button
- className="button-link issue-action issue-action-with-options js-issue-show-changelog"
- onClick={this.handleClick}>
- <span className="issue-meta-label">
- <DateFromNow date={this.props.creationDate} />
- </span>
- <i className="icon-dropdown little-spacer-left" />
- </button>
- </Tooltip>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen}
+ overlay={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}>
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay={<DateTimeFormatter date={this.props.creationDate} />}>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+ onClick={this.handleClick}>
+ <span className="issue-meta-label">
+ <DateFromNow date={this.props.creationDate} />
+ </span>
+ <i className="icon-dropdown little-spacer-left" />
+ </Button>
+ </Tooltip>
+ </Toggler>
+ </div>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
index 01ba1f984a1..814d4053cf4 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
@@ -20,7 +20,8 @@
// @flow
import React from 'react';
import { updateIssue } from '../actions';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import Toggler from '../../../components/controls/Toggler';
+import { Button } from '../../../components/ui/buttons';
import CommentPopup from '../popups/CommentPopup';
import { addIssueComment } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
@@ -49,29 +50,33 @@ export default class IssueCommentAction extends React.PureComponent {
this.props.toggleComment(false);
};
- handleCommentClick = () => this.props.toggleComment();
+ handleCommentClick = () => {
+ this.props.toggleComment();
+ };
+
+ handleClose = () => {
+ this.props.toggleComment(false);
+ };
render() {
return (
- <li className="issue-meta">
- <BubblePopupHelper
- isOpen={this.props.currentPopup === 'comment'}
- position="bottomleft"
- togglePopup={this.props.toggleComment}
- popup={
+ <li className="issue-meta dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.currentPopup === 'comment'}
+ overlay={
<CommentPopup
- customClass="issue-comment-bubble-popup"
- placeholder={this.props.commentPlaceholder}
onComment={this.addComment}
+ placeholder={this.props.commentPlaceholder}
toggleComment={this.props.toggleComment}
/>
}>
- <button
+ <Button
className="button-link issue-action js-issue-comment"
onClick={this.handleCommentClick}>
<span className="issue-meta-label">{translate('issue.comment.formlink')}</span>
- </button>
- </BubblePopupHelper>
+ </Button>
+ </Toggler>
</li>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
index 1773bd16a0c..3a0910f6552 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import Avatar from '../../../components/ui/Avatar';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import Toggler from '../../../components/controls/Toggler';
import EditIcon from '../../../components/icons-components/EditIcon';
import { EditButton, DeleteButton } from '../../../components/ui/buttons';
import CommentDeletePopup from '../popups/CommentDeletePopup';
@@ -48,12 +48,6 @@ export default class IssueCommentLine extends React.PureComponent {
openPopup: ''
};
- handleCommentClick = (event /*: Event & {target: HTMLElement}*/) => {
- if (event.target.tagName === 'A') {
- event.stopPropagation();
- }
- };
-
handleEdit = (text /*: string */) => {
this.props.onEdit(this.props.comment.key, text);
this.toggleEditPopup(false);
@@ -75,9 +69,17 @@ export default class IssueCommentLine extends React.PureComponent {
});
};
- toggleDeletePopup = (force /*: ?boolean */) => this.togglePopup('delete', force);
+ toggleDeletePopup = (force /*: ?boolean */) => {
+ this.togglePopup('delete', force);
+ };
- toggleEditPopup = (force /*: ?boolean */) => this.togglePopup('edit', force);
+ toggleEditPopup = (force /*: ?boolean */) => {
+ this.togglePopup('edit', force);
+ };
+
+ closePopups = () => {
+ this.setState({ openPopup: '' });
+ };
render() {
const { comment } = this.props;
@@ -95,49 +97,45 @@ export default class IssueCommentLine extends React.PureComponent {
<div
className="issue-comment-text markdown"
dangerouslySetInnerHTML={{ __html: comment.htmlText }}
- onClick={this.handleCommentClick}
- role="Listitem"
- tabIndex={0}
/>
<div className="issue-comment-age">
<DateFromNow date={comment.createdAt} />
</div>
<div className="issue-comment-actions">
{comment.updatable && (
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={this.state.openPopup === 'edit'}
- offset={{ vertical: 0, horizontal: -6 }}
- position="bottomright"
- togglePopup={this.toggleDeletePopup}
- popup={
- <CommentPopup
- comment={comment}
- customClass="issue-edit-comment-bubble-popup"
- onComment={this.handleEdit}
- placeholder=""
- toggleComment={this.toggleEditPopup}
+ <div className="dropdown">
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={this.closePopups}
+ open={this.state.openPopup === 'edit'}
+ overlay={
+ <CommentPopup
+ comment={comment}
+ onComment={this.handleEdit}
+ placeholder=""
+ toggleComment={this.toggleEditPopup}
+ />
+ }>
+ <EditButton
+ className="js-issue-comment-edit button-small"
+ onClick={this.toggleEditPopup}
/>
- }>
- <EditButton
- className="js-issue-comment-edit button-small"
- onClick={this.toggleEditPopup}
- />
- </BubblePopupHelper>
+ </Toggler>
+ </div>
)}
{comment.updatable && (
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={this.state.openPopup === 'delete'}
- offset={{ vertical: 0, horizontal: -10 }}
- position="bottomright"
- togglePopup={this.toggleDeletePopup}
- popup={<CommentDeletePopup onDelete={this.handleDelete} />}>
- <DeleteButton
- className="js-issue-comment-delete button-small"
- onClick={this.toggleDeletePopup}
- />
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={this.closePopups}
+ open={this.state.openPopup === 'delete'}
+ overlay={<CommentDeletePopup onDelete={this.handleDelete} />}>
+ <DeleteButton
+ className="js-issue-comment-delete button-small"
+ onClick={this.toggleDeletePopup}
+ />
+ </Toggler>
+ </div>
)}
</div>
</div>
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
index 888d7f2d521..1f8a9cb2ae5 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
@@ -38,7 +38,6 @@ export default class IssueMessage extends React.PureComponent {
handleClick = (e /*: MouseEvent */) => {
e.preventDefault();
- e.stopPropagation();
this.context.workspace.openRule({
key: this.props.rule,
organization: this.props.organization
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
index 326e64e64ab..f75e0e8f3c7 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
@@ -19,10 +19,12 @@
*/
// @flow
import React from 'react';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetSeverityPopup from '../popups/SetSeverityPopup';
-import SeverityHelper from '../../../components/shared/SeverityHelper';
import { setIssueSeverity } from '../../../api/issues';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import SeverityHelper from '../../../components/shared/SeverityHelper';
+import { Button } from '../../../components/ui/buttons';
/*:: import type { Issue } from '../types'; */
/*::
@@ -42,28 +44,31 @@ export default class IssueSeverity extends React.PureComponent {
this.props.togglePopup('set-severity', open);
};
- setSeverity = (severity /*: string */) =>
+ setSeverity = (severity /*: string */) => {
this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity);
+ };
+
+ handleClose = () => {
+ this.toggleSetSeverity(false);
+ };
render() {
const { issue } = this.props;
if (this.props.canSetSeverity) {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen && this.props.canSetSeverity}
- position="bottomleft"
- togglePopup={this.toggleSetSeverity}
- popup={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-severity"
- onClick={this.toggleSetSeverity}>
- <SeverityHelper
- className="issue-meta-label little-spacer-right"
- severity={issue.severity}
- />
- <i className="little-spacer-left icon-dropdown" />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen && this.props.canSetSeverity}
+ overlay={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-severity"
+ onClick={this.toggleSetSeverity}>
+ <SeverityHelper className="issue-meta-label" severity={issue.severity} />
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+ </Toggler>
+ </div>
);
} else {
return <SeverityHelper className="issue-meta-label" severity={issue.severity} />;
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
index fb183a7f8b7..b9f1fcba0be 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
@@ -20,10 +20,11 @@
// @flow
import React from 'react';
import { updateIssue } from '../actions';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetIssueTagsPopup from '../popups/SetIssueTagsPopup';
-import TagsList from '../../../components/tags/TagsList';
import { setIssueTags } from '../../../api/issues';
+import Toggler from '../../../components/controls/Toggler';
+import TagsList from '../../../components/tags/TagsList';
+import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
/*:: import type { Issue } from '../types'; */
@@ -57,32 +58,39 @@ export default class IssueTags extends React.PureComponent {
);
};
+ handleClose = () => {
+ this.toggleSetTags(false);
+ };
+
render() {
const { issue } = this.props;
const { tags = [] } = issue;
if (this.props.canSetTags) {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen}
- popup={
- <SetIssueTagsPopup
- organization={issue.projectOrganization}
- selectedTags={tags}
- setTags={this.setTags}
- />
- }
- position="bottomright"
- togglePopup={this.toggleSetTags}>
- <button
- className={'js-issue-edit-tags button-link issue-action issue-action-with-options'}
- onClick={this.toggleSetTags}>
- <TagsList
- allowUpdate={this.props.canSetTags}
- tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]}
- />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen}
+ overlay={
+ <SetIssueTagsPopup
+ organization={issue.projectOrganization}
+ selectedTags={tags}
+ setTags={this.setTags}
+ />
+ }>
+ <Button
+ className={'js-issue-edit-tags button-link issue-action issue-action-with-options'}
+ onClick={this.toggleSetTags}>
+ <TagsList
+ allowUpdate={this.props.canSetTags}
+ tags={
+ issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]
+ }
+ />
+ </Button>
+ </Toggler>
+ </div>
);
} else {
return (
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
index 0dad342b1b5..781eebd1f06 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
@@ -45,8 +45,6 @@ type Props = {|
|};
*/
-const stopPropagation = (event /*: Event */) => event.stopPropagation();
-
export default function IssueTitleBar(props /*: Props */) {
const { issue } = props;
const hasSimilarIssuesFilter = props.onFilter != null;
@@ -103,7 +101,7 @@ export default function IssueTitleBar(props /*: Props */) {
{displayLocations && (
<li className="issue-meta">
{props.displayLocationsLink ? (
- <Link onClick={stopPropagation} target="_blank" to={issueUrl}>
+ <Link target="_blank" to={issueUrl}>
{locationsBadge}
</Link>
) : (
@@ -112,11 +110,7 @@ export default function IssueTitleBar(props /*: Props */) {
</li>
)}
<li className="issue-meta">
- <Link
- className="js-issue-permalink link-no-underline"
- onClick={stopPropagation}
- target="_blank"
- to={issueUrl}>
+ <Link className="js-issue-permalink link-no-underline" target="_blank" to={issueUrl}>
<LinkIcon />
</Link>
</li>
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
index 28379cde1cd..3aced6f8685 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
@@ -20,10 +20,12 @@
// @flow
import React from 'react';
import { updateIssue } from '../actions';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetTransitionPopup from '../popups/SetTransitionPopup';
-import StatusHelper from '../../../components/shared/StatusHelper';
import { setIssueTransition } from '../../../api/issues';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import StatusHelper from '../../../components/shared/StatusHelper';
+import { Button } from '../../../components/ui/buttons';
/*:: import type { Issue } from '../types'; */
/*::
@@ -53,36 +55,41 @@ export default class IssueTransition extends React.PureComponent {
this.props.togglePopup('transition', open);
};
+ handleClose = () => {
+ this.toggleSetTransition(false);
+ };
+
render() {
const { issue } = this.props;
if (this.props.hasTransitions) {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen && this.props.hasTransitions}
- position="bottomleft"
- togglePopup={this.toggleSetTransition}
- popup={
- <SetTransitionPopup transitions={issue.transitions} onSelect={this.setTransition} />
- }>
- <button
- className="button-link issue-action issue-action-with-options js-issue-transition"
- onClick={this.toggleSetTransition}>
- <StatusHelper
- className="issue-meta-label little-spacer-right"
- status={issue.status}
- resolution={issue.resolution}
- />
- <i className="little-spacer-left icon-dropdown" />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen && this.props.hasTransitions}
+ overlay={
+ <SetTransitionPopup onSelect={this.setTransition} transitions={issue.transitions} />
+ }>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-transition"
+ onClick={this.toggleSetTransition}>
+ <StatusHelper
+ className="issue-meta-label"
+ resolution={issue.resolution}
+ status={issue.status}
+ />
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+ </Toggler>
+ </div>
);
} else {
return (
<StatusHelper
className="issue-meta-label"
- status={issue.status}
resolution={issue.resolution}
+ status={issue.status}
/>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.js b/server/sonar-web/src/main/js/components/issue/components/IssueType.js
index 60941288e4b..380e8668a32 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueType.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.js
@@ -19,10 +19,12 @@
*/
// @flow
import React from 'react';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
-import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SetTypePopup from '../popups/SetTypePopup';
import { setIssueType } from '../../../api/issues';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import { Button } from '../../../components/ui/buttons';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate } from '../../../helpers/l10n';
/*:: import type { Issue } from '../types'; */
@@ -43,26 +45,32 @@ export default class IssueType extends React.PureComponent {
this.props.togglePopup('set-type', open);
};
- setType = (type /*: string */) =>
+ setType = (type /*: string */) => {
this.props.setIssueProperty('type', 'set-type', setIssueType, type);
+ };
+
+ handleClose = () => {
+ this.toggleSetType(false);
+ };
render() {
const { issue } = this.props;
if (this.props.canSetSeverity) {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen && this.props.canSetSeverity}
- position="bottomleft"
- togglePopup={this.toggleSetType}
- popup={<SetTypePopup issue={issue} onSelect={this.setType} />}>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-type"
- onClick={this.toggleSetType}>
- <IssueTypeIcon className="little-spacer-right" query={issue.type} />
- {translate('issue.type', issue.type)}
- <i className="little-spacer-left icon-dropdown" />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen && this.props.canSetSeverity}
+ overlay={<SetTypePopup issue={issue} onSelect={this.setType} />}>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-type"
+ onClick={this.toggleSetType}>
+ <IssueTypeIcon className="little-spacer-right" query={issue.type} />
+ {translate('issue.type', issue.type)}
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+ </Toggler>
+ </div>
);
} else {
return (
diff --git a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
index b9da955025a..5b900ac2e48 100644
--- a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
+++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
@@ -19,8 +19,10 @@
*/
// @flow
import React from 'react';
-import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SimilarIssuesPopup from '../popups/SimilarIssuesPopup';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
/*:: import type { Issue } from '../types'; */
@@ -51,20 +53,26 @@ export default class SimilarIssuesFilter extends React.PureComponent {
this.props.togglePopup('similarIssues', open);
};
+ handleClose = () => {
+ this.togglePopup(false);
+ };
+
render() {
return (
- <BubblePopupHelper
- isOpen={this.props.isOpen}
- position="bottomright"
- togglePopup={this.togglePopup}
- popup={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}>
- <button
- className="js-issue-filter button-link issue-action issue-action-with-options"
- aria-label={translate('issue.filter_similar_issues')}
- onClick={this.handleClick}>
- <i className="icon-filter icon-half-transparent" /> <i className="icon-dropdown" />
- </button>
- </BubblePopupHelper>
+ <div className="dropdown">
+ <Toggler
+ onRequestClose={this.handleClose}
+ open={this.props.isOpen}
+ overlay={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}>
+ <Button
+ aria-label={translate('issue.filter_similar_issues')}
+ className="js-issue-filter button-link issue-action issue-action-with-options"
+ onClick={this.handleClick}>
+ <i className="icon-filter icon-half-transparent" />
+ <DropdownIcon className="little-spacer-left" />
+ </Button>
+ </Toggler>
+ </div>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
index cdd7cfeb6f1..ad39643695e 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
@@ -34,8 +34,8 @@ it('should render without the action when the correct rights are missing', () =>
canAssign={false}
isOpen={false}
issue={issue}
- onFail={jest.fn()}
onAssign={jest.fn()}
+ onFail={jest.fn()}
togglePopup={jest.fn()}
/>
);
@@ -48,8 +48,8 @@ it('should render with the action', () => {
canAssign={true}
isOpen={false}
issue={issue}
- onFail={jest.fn()}
onAssign={jest.fn()}
+ onFail={jest.fn()}
togglePopup={jest.fn()}
/>
);
@@ -63,12 +63,12 @@ it('should open the popup when the button is clicked', () => {
canAssign={true}
isOpen={false}
issue={issue}
- onFail={jest.fn()}
onAssign={jest.fn()}
+ onFail={jest.fn()}
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
index 0b2b9071cd8..8e03fdef42a 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
@@ -52,7 +52,7 @@ it('should open the popup when the button is clicked', () => {
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
index d54334a61f9..2114f366226 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
@@ -25,8 +25,8 @@ import { click } from '../../../../helpers/testUtils';
it('should render correctly', () => {
const element = shallow(
<IssueCommentAction
- issueKey="issue-key"
currentPopup={null}
+ issueKey="issue-key"
onFail={jest.fn()}
onIssueChange={jest.fn()}
toggleComment={jest.fn()}
@@ -39,14 +39,14 @@ it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueCommentAction
- issueKey="issue-key"
currentPopup={null}
+ issueKey="issue-key"
onFail={jest.fn()}
onIssueChange={jest.fn()}
toggleComment={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls.length).toBe(1);
element.setProps({ currentPopup: 'comment' });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
index a321192729a..90cc0e1d8dc 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
@@ -53,9 +53,9 @@ it('should open the right popups when the buttons are clicked', () => {
const element = shallow(
<IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} />
);
- element.find('.js-issue-comment-edit').prop('onClick')();
+ click(element.find('.js-issue-comment-edit'));
expect(element.state()).toMatchSnapshot();
- element.find('.js-issue-comment-delete').prop('onClick')();
+ click(element.find('.js-issue-comment-delete'));
expect(element.state()).toMatchSnapshot();
element.update();
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
index 98e4cf45c5b..08b00404347 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
@@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => {
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
index aa353241a8a..81d9a512a45 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
@@ -71,7 +71,7 @@ it('should open the popup when the button is clicked', () => {
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
index 9bfc5d7cd85..98da66e0449 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
@@ -84,7 +84,7 @@ it('should open the popup when the button is clicked', () => {
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
index de6dcde42bf..51132f338ca 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
@@ -63,7 +63,7 @@ it('should open the popup when the button is clicked', () => {
togglePopup={toggle}
/>
);
- click(element.find('button'));
+ click(element.find('Button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
index d4ff90579c6..d768c0c852d 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
@@ -4,112 +4,111 @@ exports[`should open the popup when the button is clicked 1`] = `
Array [
Array [
"assign",
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
+ undefined,
],
]
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <Connect(SetAssigneePopup)
- issue={
- Object {
- "assignee": "john",
- "assigneeAvatar": "gravatarhash",
- "assigneeName": "John Doe",
- }
- }
- onFail={[MockFunction]}
- onSelect={[MockFunction]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-assign"
- onClick={[Function]}
+ <Toggler
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <Connect(SetAssigneePopup)
+ issue={
+ Object {
+ "assignee": "john",
+ "assigneeAvatar": "gravatarhash",
+ "assigneeName": "John Doe",
+ }
+ }
+ onFail={[MockFunction]}
+ onSelect={[MockFunction]}
+ />
+ }
>
- <span>
- <span
- className="text-top"
- >
- <Connect(Avatar)
- className="little-spacer-right"
- hash="gravatarhash"
- name="John Doe"
- size={16}
- />
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-assign"
+ onClick={[Function]}
+ >
+ <span>
+ <span
+ className="text-top"
+ >
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="gravatarhash"
+ name="John Doe"
+ size={16}
+ />
+ </span>
+ <span
+ className="issue-meta-label"
+ >
+ John Doe
+ </span>
</span>
- <span
- className="issue-meta-label"
- >
- John Doe
- </span>
- </span>
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with the action 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <Connect(SetAssigneePopup)
- issue={
- Object {
- "assignee": "john",
- "assigneeAvatar": "gravatarhash",
- "assigneeName": "John Doe",
- }
- }
- onFail={[MockFunction]}
- onSelect={[MockFunction]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-assign"
- onClick={[Function]}
+ <Toggler
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <Connect(SetAssigneePopup)
+ issue={
+ Object {
+ "assignee": "john",
+ "assigneeAvatar": "gravatarhash",
+ "assigneeName": "John Doe",
+ }
+ }
+ onFail={[MockFunction]}
+ onSelect={[MockFunction]}
+ />
+ }
>
- <span>
- <span
- className="text-top"
- >
- <Connect(Avatar)
- className="little-spacer-right"
- hash="gravatarhash"
- name="John Doe"
- size={16}
- />
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-assign"
+ onClick={[Function]}
+ >
+ <span>
+ <span
+ className="text-top"
+ >
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="gravatarhash"
+ name="John Doe"
+ size={16}
+ />
+ </span>
+ <span
+ className="issue-meta-label"
+ >
+ John Doe
+ </span>
</span>
- <span
- className="issue-meta-label"
- >
- John Doe
- </span>
- </span>
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render without the action when the correct rights are missing 1`] = `
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
index 97f8c58d32a..0628de785bf 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
@@ -10,91 +10,97 @@ Array [
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <ChangelogPopup
- issue={
- Object {
- "author": "john.david.dalton@gmail.com",
- "creationDate": "2017-03-01T09:36:01+0100",
- "key": "issuekey",
- }
- }
- onFail={[MockFunction]}
- />
- }
- position="bottomright"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <Tooltip
- mouseEnterDelay={0.5}
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
overlay={
- <DateTimeFormatter
- date="2017-03-01T09:36:01+0100"
+ <ChangelogPopup
+ issue={
+ Object {
+ "author": "john.david.dalton@gmail.com",
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "key": "issuekey",
+ }
+ }
+ onFail={[MockFunction]}
/>
}
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-show-changelog"
- onClick={[Function]}
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay={
+ <DateTimeFormatter
+ date="2017-03-01T09:36:01+0100"
+ />
+ }
>
- <span
- className="issue-meta-label"
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+ onClick={[Function]}
>
- <DateFromNow
- date="2017-03-01T09:36:01+0100"
+ <span
+ className="issue-meta-label"
+ >
+ <DateFromNow
+ date="2017-03-01T09:36:01+0100"
+ />
+ </span>
+ <i
+ className="icon-dropdown little-spacer-left"
/>
- </span>
- <i
- className="icon-dropdown little-spacer-left"
- />
- </button>
- </Tooltip>
-</BubblePopupHelper>
+ </Button>
+ </Tooltip>
+ </Toggler>
+</div>
`;
exports[`should render correctly 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <ChangelogPopup
- issue={
- Object {
- "author": "john.david.dalton@gmail.com",
- "creationDate": "2017-03-01T09:36:01+0100",
- "key": "issuekey",
- }
- }
- onFail={[MockFunction]}
- />
- }
- position="bottomright"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <Tooltip
- mouseEnterDelay={0.5}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
overlay={
- <DateTimeFormatter
- date="2017-03-01T09:36:01+0100"
+ <ChangelogPopup
+ issue={
+ Object {
+ "author": "john.david.dalton@gmail.com",
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "key": "issuekey",
+ }
+ }
+ onFail={[MockFunction]}
/>
}
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-show-changelog"
- onClick={[Function]}
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay={
+ <DateTimeFormatter
+ date="2017-03-01T09:36:01+0100"
+ />
+ }
>
- <span
- className="issue-meta-label"
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+ onClick={[Function]}
>
- <DateFromNow
- date="2017-03-01T09:36:01+0100"
+ <span
+ className="issue-meta-label"
+ >
+ <DateFromNow
+ date="2017-03-01T09:36:01+0100"
+ />
+ </span>
+ <i
+ className="icon-dropdown little-spacer-left"
/>
- </span>
- <i
- className="icon-dropdown little-spacer-left"
- />
- </button>
- </Tooltip>
-</BubblePopupHelper>
+ </Button>
+ </Tooltip>
+ </Toggler>
+</div>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
index 7305e5d8472..3c62307690e 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
@@ -2,13 +2,13 @@
exports[`should open the popup when the button is clicked 1`] = `
<li
- className="issue-meta"
+ className="issue-meta dropdown"
>
- <BubblePopupHelper
- isOpen={true}
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
<CommentPopup
- customClass="issue-comment-bubble-popup"
onComment={[Function]}
placeholder={undefined}
toggleComment={
@@ -20,16 +20,8 @@ exports[`should open the popup when the button is clicked 1`] = `
}
/>
}
- position="bottomleft"
- togglePopup={
- [MockFunction] {
- "calls": Array [
- Array [],
- ],
- }
- }
>
- <button
+ <Button
className="button-link issue-action js-issue-comment"
onClick={[Function]}
>
@@ -38,29 +30,27 @@ exports[`should open the popup when the button is clicked 1`] = `
>
issue.comment.formlink
</span>
- </button>
- </BubblePopupHelper>
+ </Button>
+ </Toggler>
</li>
`;
exports[`should render correctly 1`] = `
<li
- className="issue-meta"
+ className="issue-meta dropdown"
>
- <BubblePopupHelper
- isOpen={false}
- popup={
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
<CommentPopup
- customClass="issue-comment-bubble-popup"
onComment={[Function]}
placeholder={undefined}
toggleComment={[MockFunction]}
/>
}
- position="bottomleft"
- togglePopup={[MockFunction]}
>
- <button
+ <Button
className="button-link issue-action js-issue-comment"
onClick={[Function]}
>
@@ -69,7 +59,7 @@ exports[`should render correctly 1`] = `
>
issue.comment.formlink
</span>
- </button>
- </BubblePopupHelper>
+ </Button>
+ </Toggler>
</li>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
index 71f4ef39f22..4db5ed5aa9c 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
@@ -35,9 +35,6 @@ exports[`should open the right popups when the buttons are clicked 3`] = `
"__html": "<b>test</b>",
}
}
- onClick={[Function]}
- role="Listitem"
- tabIndex={0}
/>
<div
className="issue-comment-age"
@@ -49,63 +46,56 @@ exports[`should open the right popups when the buttons are clicked 3`] = `
<div
className="issue-comment-actions"
>
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={false}
- offset={
- Object {
- "horizontal": -6,
- "vertical": 0,
- }
- }
- popup={
- <CommentPopup
- comment={
- Object {
- "authorAvatar": "gravatarhash",
- "authorName": "John Doe",
- "createdAt": "2017-03-01T09:36:01+0100",
- "htmlText": "<b>test</b>",
- "key": "comment-key",
- "updatable": true,
+ <div
+ className="dropdown"
+ >
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <CommentPopup
+ comment={
+ Object {
+ "authorAvatar": "gravatarhash",
+ "authorName": "John Doe",
+ "createdAt": "2017-03-01T09:36:01+0100",
+ "htmlText": "<b>test</b>",
+ "key": "comment-key",
+ "updatable": true,
+ }
}
- }
- customClass="issue-edit-comment-bubble-popup"
- onComment={[Function]}
- placeholder=""
- toggleComment={[Function]}
+ onComment={[Function]}
+ placeholder=""
+ toggleComment={[Function]}
+ />
+ }
+ >
+ <EditButton
+ className="js-issue-comment-edit button-small"
+ onClick={[Function]}
/>
- }
- position="bottomright"
- togglePopup={[Function]}
+ </Toggler>
+ </div>
+ <div
+ className="dropdown"
>
- <EditButton
- className="js-issue-comment-edit button-small"
- onClick={[Function]}
- />
- </BubblePopupHelper>
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={true}
- offset={
- Object {
- "horizontal": -10,
- "vertical": 0,
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <CommentDeletePopup
+ onDelete={[Function]}
+ />
}
- }
- popup={
- <CommentDeletePopup
- onDelete={[Function]}
+ >
+ <DeleteButton
+ className="js-issue-comment-delete button-small"
+ onClick={[Function]}
/>
- }
- position="bottomright"
- togglePopup={[Function]}
- >
- <DeleteButton
- className="js-issue-comment-delete button-small"
- onClick={[Function]}
- />
- </BubblePopupHelper>
+ </Toggler>
+ </div>
</div>
</div>
`;
@@ -133,9 +123,6 @@ exports[`should render correctly a comment that is not updatable 1`] = `
"__html": "<b>test</b>",
}
}
- onClick={[Function]}
- role="Listitem"
- tabIndex={0}
/>
<div
className="issue-comment-age"
@@ -173,9 +160,6 @@ exports[`should render correctly a comment that is updatable 1`] = `
"__html": "<b>test</b>",
}
}
- onClick={[Function]}
- role="Listitem"
- tabIndex={0}
/>
<div
className="issue-comment-age"
@@ -187,63 +171,56 @@ exports[`should render correctly a comment that is updatable 1`] = `
<div
className="issue-comment-actions"
>
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={false}
- offset={
- Object {
- "horizontal": -6,
- "vertical": 0,
- }
- }
- popup={
- <CommentPopup
- comment={
- Object {
- "authorAvatar": "gravatarhash",
- "authorName": "John Doe",
- "createdAt": "2017-03-01T09:36:01+0100",
- "htmlText": "<b>test</b>",
- "key": "comment-key",
- "updatable": true,
+ <div
+ className="dropdown"
+ >
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <CommentPopup
+ comment={
+ Object {
+ "authorAvatar": "gravatarhash",
+ "authorName": "John Doe",
+ "createdAt": "2017-03-01T09:36:01+0100",
+ "htmlText": "<b>test</b>",
+ "key": "comment-key",
+ "updatable": true,
+ }
}
- }
- customClass="issue-edit-comment-bubble-popup"
- onComment={[Function]}
- placeholder=""
- toggleComment={[Function]}
+ onComment={[Function]}
+ placeholder=""
+ toggleComment={[Function]}
+ />
+ }
+ >
+ <EditButton
+ className="js-issue-comment-edit button-small"
+ onClick={[Function]}
/>
- }
- position="bottomright"
- togglePopup={[Function]}
+ </Toggler>
+ </div>
+ <div
+ className="dropdown"
>
- <EditButton
- className="js-issue-comment-edit button-small"
- onClick={[Function]}
- />
- </BubblePopupHelper>
- <BubblePopupHelper
- className="bubble-popup-helper-inline"
- isOpen={false}
- offset={
- Object {
- "horizontal": -10,
- "vertical": 0,
+ <Toggler
+ className="display-inline-block"
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <CommentDeletePopup
+ onDelete={[Function]}
+ />
}
- }
- popup={
- <CommentDeletePopup
- onDelete={[Function]}
+ >
+ <DeleteButton
+ className="js-issue-comment-delete button-small"
+ onClick={[Function]}
/>
- }
- position="bottomright"
- togglePopup={[Function]}
- >
- <DeleteButton
- className="js-issue-comment-delete button-small"
- onClick={[Function]}
- />
- </BubblePopupHelper>
+ </Toggler>
+ </div>
</div>
</div>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
index 98eb8644b75..898a75fd3d6 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
@@ -4,80 +4,77 @@ exports[`should open the popup when the button is clicked 1`] = `
Array [
Array [
"set-severity",
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
+ undefined,
],
]
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <SetSeverityPopup
- issue={
- Object {
- "severity": "BLOCKER",
- }
- }
- onSelect={[Function]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-severity"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <SetSeverityPopup
+ issue={
+ Object {
+ "severity": "BLOCKER",
+ }
+ }
+ onSelect={[Function]}
+ />
+ }
>
- <SeverityHelper
- className="issue-meta-label little-spacer-right"
- severity="BLOCKER"
- />
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-severity"
+ onClick={[Function]}
+ >
+ <SeverityHelper
+ className="issue-meta-label"
+ severity="BLOCKER"
+ />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with the action 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <SetSeverityPopup
- issue={
- Object {
- "severity": "BLOCKER",
- }
- }
- onSelect={[Function]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-severity"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <SetSeverityPopup
+ issue={
+ Object {
+ "severity": "BLOCKER",
+ }
+ }
+ onSelect={[Function]}
+ />
+ }
>
- <SeverityHelper
- className="issue-meta-label little-spacer-right"
- severity="BLOCKER"
- />
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-severity"
+ onClick={[Function]}
+ >
+ <SeverityHelper
+ className="issue-meta-label"
+ severity="BLOCKER"
+ />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render without the action when the correct rights are missing 1`] = `
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
index fa30aff7c20..ca33fb7f1ec 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
@@ -4,88 +4,85 @@ exports[`should open the popup when the button is clicked 1`] = `
Array [
Array [
"edit-tags",
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
+ undefined,
],
]
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <SetIssueTagsPopup
- organization="foo"
- selectedTags={
- Array [
- "mytag",
- "test",
- ]
- }
- setTags={[Function]}
- />
- }
- position="bottomright"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="js-issue-edit-tags button-link issue-action issue-action-with-options"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <SetIssueTagsPopup
+ organization="foo"
+ selectedTags={
+ Array [
+ "mytag",
+ "test",
+ ]
+ }
+ setTags={[Function]}
+ />
+ }
>
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "mytag",
- "test",
- ]
- }
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="js-issue-edit-tags button-link issue-action issue-action-with-options"
+ onClick={[Function]}
+ >
+ <TagsList
+ allowUpdate={true}
+ tags={
+ Array [
+ "mytag",
+ "test",
+ ]
+ }
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with the action 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <SetIssueTagsPopup
- organization="foo"
- selectedTags={
- Array [
- "mytag",
- "test",
- ]
- }
- setTags={[Function]}
- />
- }
- position="bottomright"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="js-issue-edit-tags button-link issue-action issue-action-with-options"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <SetIssueTagsPopup
+ organization="foo"
+ selectedTags={
+ Array [
+ "mytag",
+ "test",
+ ]
+ }
+ setTags={[Function]}
+ />
+ }
>
- <TagsList
- allowUpdate={true}
- tags={
- Array [
- "mytag",
- "test",
- ]
- }
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="js-issue-edit-tags button-link issue-action issue-action-with-options"
+ onClick={[Function]}
+ >
+ <TagsList
+ allowUpdate={true}
+ tags={
+ Array [
+ "mytag",
+ "test",
+ ]
+ }
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render without the action when the correct rights are missing 1`] = `
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
index 30fc9e84c33..8f3694dd8d0 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
@@ -68,7 +68,6 @@ exports[`should render the titlebar correctly 1`] = `
>
<Link
className="js-issue-permalink link-no-underline"
- onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
@@ -152,7 +151,6 @@ exports[`should render the titlebar with the filter 1`] = `
>
<Link
className="js-issue-permalink link-no-underline"
- onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
index cd14db53dd2..dcfe4ff35ea 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
@@ -4,118 +4,118 @@ exports[`should open the popup when the button is clicked 1`] = `
Array [
Array [
"transition",
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
+ undefined,
],
]
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <SetTransitionPopup
- onSelect={[Function]}
- transitions={
- Array [
- "confirm",
- "resolve",
- "falsepositive",
- "wontfix",
- ]
- }
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-transition"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <SetTransitionPopup
+ onSelect={[Function]}
+ transitions={
+ Array [
+ "confirm",
+ "resolve",
+ "falsepositive",
+ "wontfix",
+ ]
+ }
+ />
+ }
>
- <StatusHelper
- className="issue-meta-label little-spacer-right"
- status="OPEN"
- />
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-transition"
+ onClick={[Function]}
+ >
+ <StatusHelper
+ className="issue-meta-label"
+ status="OPEN"
+ />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with a resolution 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <SetTransitionPopup
- onSelect={[Function]}
- transitions={
- Array [
- "reopen",
- ]
- }
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-transition"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <SetTransitionPopup
+ onSelect={[Function]}
+ transitions={
+ Array [
+ "reopen",
+ ]
+ }
+ />
+ }
>
- <StatusHelper
- className="issue-meta-label little-spacer-right"
- resolution="FIXED"
- status="RESOLVED"
- />
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-transition"
+ onClick={[Function]}
+ >
+ <StatusHelper
+ className="issue-meta-label"
+ resolution="FIXED"
+ status="RESOLVED"
+ />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with the action 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <SetTransitionPopup
- onSelect={[Function]}
- transitions={
- Array [
- "confirm",
- "resolve",
- "falsepositive",
- "wontfix",
- ]
- }
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-transition"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <SetTransitionPopup
+ onSelect={[Function]}
+ transitions={
+ Array [
+ "confirm",
+ "resolve",
+ "falsepositive",
+ "wontfix",
+ ]
+ }
+ />
+ }
>
- <StatusHelper
- className="issue-meta-label little-spacer-right"
- status="OPEN"
- />
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-transition"
+ onClick={[Function]}
+ >
+ <StatusHelper
+ className="issue-meta-label"
+ status="OPEN"
+ />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render without the action when there is no transitions 1`] = `
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
index 77988d1f003..1eb2844dbf6 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
@@ -4,82 +4,79 @@ exports[`should open the popup when the button is clicked 1`] = `
Array [
Array [
"set-type",
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
+ undefined,
],
]
`;
exports[`should open the popup when the button is clicked 2`] = `
-<BubblePopupHelper
- isOpen={true}
- popup={
- <SetTypePopup
- issue={
- Object {
- "type": "bug",
- }
- }
- onSelect={[Function]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-type"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <SetTypePopup
+ issue={
+ Object {
+ "type": "bug",
+ }
+ }
+ onSelect={[Function]}
+ />
+ }
>
- <IssueTypeIcon
- className="little-spacer-right"
- query="bug"
- />
- issue.type.bug
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-type"
+ onClick={[Function]}
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="bug"
+ />
+ issue.type.bug
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render with the action 1`] = `
-<BubblePopupHelper
- isOpen={false}
- popup={
- <SetTypePopup
- issue={
- Object {
- "type": "bug",
- }
- }
- onSelect={[Function]}
- />
- }
- position="bottomleft"
- togglePopup={[Function]}
+<div
+ className="dropdown"
>
- <button
- className="button-link issue-action issue-action-with-options js-issue-set-type"
- onClick={[Function]}
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <SetTypePopup
+ issue={
+ Object {
+ "type": "bug",
+ }
+ }
+ onSelect={[Function]}
+ />
+ }
>
- <IssueTypeIcon
- className="little-spacer-right"
- query="bug"
- />
- issue.type.bug
- <i
- className="little-spacer-left icon-dropdown"
- />
- </button>
-</BubblePopupHelper>
+ <Button
+ className="button-link issue-action issue-action-with-options js-issue-set-type"
+ onClick={[Function]}
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="bug"
+ />
+ issue.type.bug
+ <DropdownIcon
+ className="little-spacer-left"
+ />
+ </Button>
+ </Toggler>
+</div>
`;
exports[`should render without the action when the correct rights are missing 1`] = `
diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
index b3f230810fe..fcfd515b587 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
@@ -22,9 +22,9 @@ import React from 'react';
import { getIssueChangelog } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import Avatar from '../../../components/ui/Avatar';
-import BubblePopup from '../../../components/common/BubblePopup';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import IssueChangelogDiff from '../components/IssueChangelogDiff';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*:: import type { ChangelogDiff } from '../components/IssueChangelogDiff'; */
/*:: import type { Issue } from '../types'; */
@@ -80,8 +80,8 @@ export default class ChangelogPopup extends React.PureComponent {
const { issue } = this.props;
const { author } = issue;
return (
- <BubblePopup position={this.props.popupPosition} customClass="bubble-popup-bottom-right">
- <div className="issue-changelog">
+ <DropdownOverlay>
+ <div className="menu is-container issue-changelog">
<table className="spaced">
<tbody>
<tr>
@@ -110,14 +110,14 @@ export default class ChangelogPopup extends React.PureComponent {
{item.userName}
</p>
)}
- {item.diffs.map(diff => <IssueChangelogDiff key={diff.key} diff={diff} />)}
+ {item.diffs.map(diff => <IssueChangelogDiff diff={diff} key={diff.key} />)}
</td>
</tr>
))}
</tbody>
</table>
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
index 9776ab00253..c7fd5d89ae8 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
@@ -19,8 +19,9 @@
*/
// @flow
import React from 'react';
+import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
-import BubblePopup from '../../../components/common/BubblePopup';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*::
type Props = {
@@ -31,13 +32,13 @@ type Props = {
export default function CommentDeletePopup(props /*: Props */) {
return (
- <BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom-right">
- <div className="text-right">
+ <DropdownOverlay>
+ <div className="menu is-container">
<div className="spacer-bottom">{translate('issue.comment.delete_confirm_message')}</div>
- <button className="button-red" onClick={props.onDelete}>
+ <Button className="button-red" onClick={props.onDelete}>
{translate('delete')}
- </button>
+ </Button>
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
index 2a17f71383b..bee719a083e 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
@@ -19,16 +19,15 @@
*/
// @flow
import React from 'react';
-import classNames from 'classnames';
-import BubblePopup from '../../../components/common/BubblePopup';
import MarkdownTips from '../../../components/common/MarkdownTips';
+import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*:: import type { IssueComment } from '../types'; */
/*::
type Props = {
comment?: IssueComment,
- customClass?: string,
onComment: string => void,
toggleComment: boolean => void,
placeholder: string,
@@ -63,8 +62,7 @@ export default class CommentPopup extends React.PureComponent {
}
};
- handleCancelClick = (evt /*: MouseEvent */) => {
- evt.preventDefault();
+ handleCancelClick = () => {
this.props.toggleComment(false);
};
@@ -81,37 +79,37 @@ export default class CommentPopup extends React.PureComponent {
render() {
const { comment } = this.props;
return (
- <BubblePopup
- position={this.props.popupPosition}
- customClass={classNames(this.props.customClass, 'bubble-popup-bottom-right')}>
- <div className="issue-comment-form-text">
- <textarea
- autoFocus={true}
- placeholder={this.props.placeholder}
- onChange={this.handleCommentChange}
- onKeyDown={this.handleKeyboard}
- value={this.state.textComment}
- rows="2"
- />
- </div>
- <div className="issue-comment-form-footer">
- <div className="issue-comment-form-actions">
- <button
- className="js-issue-comment-submit little-spacer-right"
- disabled={this.state.textComment.trim().length < 1}
- onClick={this.handleCommentClick}>
- {comment && translate('save')}
- {!comment && translate('issue.comment.submit')}
- </button>
- <a href="#" className="js-issue-comment-cancel" onClick={this.handleCancelClick}>
- {translate('cancel')}
- </a>
+ <DropdownOverlay>
+ <div className="issue-comment-bubble-popup">
+ <div className="issue-comment-form-text">
+ <textarea
+ autoFocus={true}
+ onChange={this.handleCommentChange}
+ onKeyDown={this.handleKeyboard}
+ placeholder={this.props.placeholder}
+ rows="2"
+ value={this.state.textComment}
+ />
</div>
- <div className="issue-comment-form-tips">
- <MarkdownTips />
+ <div className="issue-comment-form-footer">
+ <div className="issue-comment-form-actions">
+ <Button
+ className="js-issue-comment-submit little-spacer-right"
+ disabled={this.state.textComment.trim().length < 1}
+ onClick={this.handleCommentClick}>
+ {comment && translate('save')}
+ {!comment && translate('issue.comment.submit')}
+ </Button>
+ <ResetButtonLink className="js-issue-comment-cancel" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </div>
+ <div className="issue-comment-form-tips">
+ <MarkdownTips />
+ </div>
</div>
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
index 270cbe04158..b5658c997d3 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
@@ -23,7 +23,6 @@ import { map } from 'lodash';
import { connect } from 'react-redux';
import * as PropTypes from 'prop-types';
import Avatar from '../../../components/ui/Avatar';
-import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import SearchBox from '../../../components/controls/SearchBox';
@@ -31,6 +30,7 @@ import { searchMembers } from '../../../api/organizations';
import { searchUsers } from '../../../api/users';
import { translate } from '../../../helpers/l10n';
import { getCurrentUser } from '../../../store/rootReducer';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*:: import type { Issue } from '../types'; */
/*::
@@ -123,9 +123,7 @@ class SetAssigneePopup extends React.PureComponent {
render() {
return (
- <BubblePopup
- customClass="bubble-popup-menu bubble-popup-bottom"
- position={this.props.popupPosition}>
+ <DropdownOverlay noPadding={true}>
<div className="multi-select">
<div className="menu-search">
<SearchBox
@@ -142,7 +140,7 @@ class SetAssigneePopup extends React.PureComponent {
items={map(this.state.users, 'login')}
onSelect={this.props.onSelect}>
{this.state.users.map(user => (
- <SelectListItem key={user.login} item={user.login}>
+ <SelectListItem item={user.login} key={user.login}>
{!!user.login && (
<Avatar className="spacer-right" hash={user.avatar} name={user.name} size={16} />
)}
@@ -155,7 +153,7 @@ class SetAssigneePopup extends React.PureComponent {
))}
</SelectList>
</div>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
index 90b8af5f628..c41c216e8b6 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
@@ -19,12 +19,12 @@
*/
import * as React from 'react';
import { difference, without } from 'lodash';
-import { BubblePopupPosition } from '../../../components/common/BubblePopup';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchIssueTags } from '../../../api/issues';
+import { DropdownOverlay } from '../../controls/Dropdown';
+import { PopupPlacement } from '../../ui/popups';
interface Props {
- popupPosition: BubblePopupPosition;
organization: string;
selectedTags: string[];
setTags: (tags: string[]) => void;
@@ -74,15 +74,16 @@ export default class SetIssueTagsPopup extends React.PureComponent<Props, State>
render() {
const availableTags = difference(this.state.searchResult, this.props.selectedTags);
return (
- <TagsSelector
- listSize={LIST_SIZE}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- position={this.props.popupPosition}
- selectedTags={this.props.selectedTags}
- tags={availableTags}
- />
+ <DropdownOverlay placement={PopupPlacement.BottomRight}>
+ <TagsSelector
+ listSize={LIST_SIZE}
+ onSearch={this.onSearch}
+ onSelect={this.onSelect}
+ onUnselect={this.onUnselect}
+ selectedTags={this.props.selectedTags}
+ tags={availableTags}
+ />
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
index cc03b290475..2d3c2232f88 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
@@ -20,17 +20,16 @@
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';
-import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import SeverityIcon from '../../../components/shared/SeverityIcon';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*:: import type { Issue } from '../types'; */
/*::
type Props = {
issue: Issue,
onSelect: string => void,
- popupPosition?: {}
};
*/
@@ -41,21 +40,19 @@ export default class SetSeverityPopup extends React.PureComponent {
render() {
return (
- <BubblePopup
- position={this.props.popupPosition}
- customClass="bubble-popup-menu bubble-popup-bottom">
+ <DropdownOverlay>
<SelectList
- items={SEVERITY}
currentItem={this.props.issue.severity}
+ items={SEVERITY}
onSelect={this.props.onSelect}>
{SEVERITY.map(severity => (
- <SelectListItem key={severity} item={severity}>
+ <SelectListItem item={severity} key={severity}>
<SeverityIcon className="little-spacer-right" severity={severity} />
{translate('severity', severity)}
</SelectListItem>
))}
</SelectList>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
index 44bf942253d..37d342f7eb2 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
@@ -19,16 +19,15 @@
*/
// @flow
import React from 'react';
-import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import { translate } from '../../../helpers/l10n';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*::
type Props = {
transitions: Array<string>,
onSelect: string => void,
- popupPosition?: {}
};
*/
@@ -38,22 +37,20 @@ export default class SetTransitionPopup extends React.PureComponent {
render() {
const { transitions } = this.props;
return (
- <BubblePopup
- position={this.props.popupPosition}
- customClass="bubble-popup-menu bubble-popup-bottom">
- <SelectList items={transitions} currentItem={transitions[0]} onSelect={this.props.onSelect}>
+ <DropdownOverlay>
+ <SelectList currentItem={transitions[0]} items={transitions} onSelect={this.props.onSelect}>
{transitions.map(transition => {
return (
<SelectListItem
- key={transition}
item={transition}
+ key={transition}
title={translate('issue.transition', transition, 'description')}>
{translate('issue.transition', transition)}
</SelectListItem>
);
})}
</SelectList>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
index f623afccd08..f69e07cf141 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
@@ -20,17 +20,16 @@
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';
-import BubblePopup from '../../../components/common/BubblePopup';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
+import { DropdownOverlay } from '../../controls/Dropdown';
/*:: import type { Issue } from '../types'; */
/*::
type Props = {
issue: Issue,
onSelect: string => void,
- popupPosition?: {}
};
*/
@@ -41,21 +40,19 @@ export default class SetTypePopup extends React.PureComponent {
render() {
return (
- <BubblePopup
- position={this.props.popupPosition}
- customClass="bubble-popup-menu bubble-popup-bottom">
+ <DropdownOverlay>
<SelectList
- items={TYPES}
currentItem={this.props.issue.type}
+ items={TYPES}
onSelect={this.props.onSelect}>
{TYPES.map(type => (
- <SelectListItem key={type} item={type}>
+ <SelectListItem item={type} key={type}>
<IssueTypeIcon className="little-spacer-right" query={type} />
{translate('issue.type', type)}
</SelectListItem>
))}
</SelectList>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
index fb9b24780e3..7a28c6594ed 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
@@ -19,9 +19,9 @@
*/
// @flow
import React from 'react';
-import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
+import { DropdownOverlay } from '../../../components/controls/Dropdown';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import StatusHelper from '../../../components/shared/StatusHelper';
import QualifierIcon from '../../../components/shared/QualifierIcon';
@@ -35,7 +35,6 @@ import { fileFromPath, limitComponentName } from '../../../helpers/path';
type Props = {|
issue: Issue,
onFilter: (property: string, issue: Issue) => void,
- popupPosition?: {}
|};
*/
@@ -64,9 +63,7 @@ export default class SimilarIssuesPopup extends React.PureComponent {
].filter(item => item);
return (
- <BubblePopup
- position={this.props.popupPosition}
- customClass="bubble-popup-menu bubble-popup-bottom-right">
+ <DropdownOverlay noPadding={true}>
<header className="menu-search">
<h6>{translate('issue.filter_similar_issues')}</h6>
</header>
@@ -118,7 +115,7 @@ export default class SimilarIssuesPopup extends React.PureComponent {
{issue.tags != null &&
issue.tags.map(tag => (
- <SelectListItem key={`tag###${tag}`} item={`tag###${tag}`}>
+ <SelectListItem item={`tag###${tag}`} key={`tag###${tag}`}>
<i className="icon-tags icon-half-transparent little-spacer-right" />
{tag}
</SelectListItem>
@@ -143,7 +140,7 @@ export default class SimilarIssuesPopup extends React.PureComponent {
{fileFromPath(issue.componentLongName)}
</SelectListItem>
</SelectList>
- </BubblePopup>
+ </DropdownOverlay>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
index 2256e3fb35a..be0c222335d 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
@@ -26,6 +26,6 @@ it('should render the comment delete popup correctly', () => {
const onDelete = jest.fn();
const element = shallow(<CommentDeletePopup onDelete={onDelete} />);
expect(element).toMatchSnapshot();
- click(element.find('button'));
+ click(element.find('Button'));
expect(onDelete.mock.calls.length).toBe(1);
});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
index 7716fe669a3..6cfd0dcdf64 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
@@ -25,10 +25,10 @@ import { click } from '../../../../helpers/testUtils';
it('should render the comment popup correctly without existing comment', () => {
const element = shallow(
<CommentPopup
+ customClass="myclass"
onComment={jest.fn()}
- toggleComment={jest.fn()}
placeholder="placeholder test"
- customClass="myclass"
+ toggleComment={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
@@ -37,12 +37,10 @@ it('should render the comment popup correctly without existing comment', () => {
it('should render the comment popup correctly when changing a comment', () => {
const element = shallow(
<CommentPopup
- comment={{
- markdown: '*test*'
- }}
+ comment={{ markdown: '*test*' }}
onComment={jest.fn()}
- toggleComment={jest.fn()}
placeholder=""
+ toggleComment={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
@@ -52,15 +50,15 @@ it('should render not allow to send comment with only spaces', () => {
const onComment = jest.fn();
const element = shallow(
<CommentPopup
+ customClass="myclass"
onComment={onComment}
- toggleComment={jest.fn()}
placeholder="placeholder test"
- customClass="myclass"
+ toggleComment={jest.fn()}
/>
);
- click(element.find('button.js-issue-comment-submit'));
+ click(element.find('.js-issue-comment-submit'));
expect(onComment.mock.calls.length).toBe(0);
element.setState({ textComment: 'mycomment' });
- click(element.find('button.js-issue-comment-submit'));
+ click(element.find('.js-issue-comment-submit'));
expect(onComment.mock.calls.length).toBe(1);
});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx
index f66e329ec87..00b85fd1817 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx
@@ -23,12 +23,7 @@ import SetIssueTagsPopup from '../SetIssueTagsPopup';
it('should render tags popup correctly', () => {
const element = shallow(
- <SetIssueTagsPopup
- organization="foo"
- popupPosition={{}}
- selectedTags={['mytag']}
- setTags={jest.fn()}
- />
+ <SetIssueTagsPopup organization="foo" selectedTags={['mytag']} setTags={jest.fn()} />
);
element.setState({ searchResult: ['mytag', 'test', 'second'] });
expect(element).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
index 65cade73cf8..f54cfd88b77 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
@@ -1,11 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render the changelog popup correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom-right"
->
+<DropdownOverlay>
<div
- className="issue-changelog"
+ className="menu is-container issue-changelog"
>
<table
className="spaced"
@@ -62,5 +60,5 @@ exports[`should render the changelog popup correctly 1`] = `
</tbody>
</table>
</div>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
index 634b35907be..43c00928943 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
@@ -1,23 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render the comment delete popup correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom-right"
->
+<DropdownOverlay>
<div
- className="text-right"
+ className="menu is-container"
>
<div
className="spacer-bottom"
>
issue.comment.delete_confirm_message
</div>
- <button
+ <Button
className="button-red"
onClick={[MockFunction]}
>
delete
- </button>
+ </Button>
</div>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
index d33898199b7..8ed5e9eb55a 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
@@ -1,93 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render the comment popup correctly when changing a comment 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom-right"
->
+<DropdownOverlay>
<div
- className="issue-comment-form-text"
- >
- <textarea
- autoFocus={true}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder=""
- rows="2"
- value="*test*"
- />
- </div>
- <div
- className="issue-comment-form-footer"
+ className="issue-comment-bubble-popup"
>
<div
- className="issue-comment-form-actions"
+ className="issue-comment-form-text"
>
- <button
- className="js-issue-comment-submit little-spacer-right"
- disabled={false}
- onClick={[Function]}
- >
- save
- </button>
- <a
- className="js-issue-comment-cancel"
- href="#"
- onClick={[Function]}
- >
- cancel
- </a>
+ <textarea
+ autoFocus={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder=""
+ rows="2"
+ value="*test*"
+ />
</div>
<div
- className="issue-comment-form-tips"
+ className="issue-comment-form-footer"
>
- <MarkdownTips />
+ <div
+ className="issue-comment-form-actions"
+ >
+ <Button
+ className="js-issue-comment-submit little-spacer-right"
+ disabled={false}
+ onClick={[Function]}
+ >
+ save
+ </Button>
+ <ResetButtonLink
+ className="js-issue-comment-cancel"
+ onClick={[Function]}
+ >
+ cancel
+ </ResetButtonLink>
+ </div>
+ <div
+ className="issue-comment-form-tips"
+ >
+ <MarkdownTips />
+ </div>
</div>
</div>
-</BubblePopup>
+</DropdownOverlay>
`;
exports[`should render the comment popup correctly without existing comment 1`] = `
-<BubblePopup
- customClass="myclass bubble-popup-bottom-right"
->
+<DropdownOverlay>
<div
- className="issue-comment-form-text"
- >
- <textarea
- autoFocus={true}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="placeholder test"
- rows="2"
- value=""
- />
- </div>
- <div
- className="issue-comment-form-footer"
+ className="issue-comment-bubble-popup"
>
<div
- className="issue-comment-form-actions"
+ className="issue-comment-form-text"
>
- <button
- className="js-issue-comment-submit little-spacer-right"
- disabled={true}
- onClick={[Function]}
- >
- issue.comment.submit
- </button>
- <a
- className="js-issue-comment-cancel"
- href="#"
- onClick={[Function]}
- >
- cancel
- </a>
+ <textarea
+ autoFocus={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="placeholder test"
+ rows="2"
+ value=""
+ />
</div>
<div
- className="issue-comment-form-tips"
+ className="issue-comment-form-footer"
>
- <MarkdownTips />
+ <div
+ className="issue-comment-form-actions"
+ >
+ <Button
+ className="js-issue-comment-submit little-spacer-right"
+ disabled={true}
+ onClick={[Function]}
+ >
+ issue.comment.submit
+ </Button>
+ <ResetButtonLink
+ className="js-issue-comment-cancel"
+ onClick={[Function]}
+ >
+ cancel
+ </ResetButtonLink>
+ </div>
+ <div
+ className="issue-comment-form-tips"
+ >
+ <MarkdownTips />
+ </div>
</div>
</div>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
index 57990df2131..a5eb2aafcac 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
@@ -1,22 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render tags popup correctly 1`] = `
-<TagsSelector
- listSize={10}
- onSearch={[Function]}
- onSelect={[Function]}
- onUnselect={[Function]}
- position={Object {}}
- selectedTags={
- Array [
- "mytag",
- ]
- }
- tags={
- Array [
- "test",
- "second",
- ]
- }
-/>
+<DropdownOverlay
+ placement="bottom-right"
+>
+ <TagsSelector
+ listSize={10}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ selectedTags={
+ Array [
+ "mytag",
+ ]
+ }
+ tags={
+ Array [
+ "test",
+ "second",
+ ]
+ }
+ />
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
index f5cc9cfed81..f8818f53cab 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
@@ -1,9 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render tags popup correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-menu bubble-popup-bottom"
->
+<DropdownOverlay>
<SelectList
currentItem="MAJOR"
items={
@@ -68,5 +66,5 @@ exports[`should render tags popup correctly 1`] = `
severity.INFO
</SelectListItem>
</SelectList>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
index 2b0be175cbe..a9728b1687a 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
@@ -1,9 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render tags popup correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-menu bubble-popup-bottom"
->
+<DropdownOverlay>
<SelectList
currentItem="confirm"
items={
@@ -45,5 +43,5 @@ exports[`should render tags popup correctly 1`] = `
issue.transition.wontfix
</SelectListItem>
</SelectList>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
index 190d91cb61b..5055d807449 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
@@ -1,9 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render tags popup correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-menu bubble-popup-bottom"
->
+<DropdownOverlay>
<SelectList
currentItem="BUG"
items={
@@ -46,5 +44,5 @@ exports[`should render tags popup correctly 1`] = `
issue.type.CODE_SMELL
</SelectListItem>
</SelectList>
-</BubblePopup>
+</DropdownOverlay>
`;
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
index ccb6c4d99ad..75ead462050 100644
--- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
@@ -18,10 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import classNames from 'classnames';
import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
-import BubblePopup from '../common/BubblePopup';
import DateFormatter from '../intl/DateFormatter';
+import { Popup, PopupPlacement } from '../ui/popups';
/*:: import type { Metric } from '../types'; */
/*:: import type { Serie } from '../charts/AdvancedTimeline'; */
@@ -47,16 +46,17 @@ export default class PreviewGraphTooltips extends React.PureComponent {
const { tooltipIdx } = this.props;
const top = 16;
let left = this.props.tooltipPos;
- let customClass;
+ let placement = PopupPlacement.RightTop;
if (left > this.props.graphWidth - TOOLTIP_WIDTH) {
left -= TOOLTIP_WIDTH;
- customClass = 'bubble-popup-right';
+ placement = PopupPlacement.LeftTop;
}
return (
- <BubblePopup
- customClass={classNames(customClass, 'disabled-pointer-events')}
- position={{ top, left, width: TOOLTIP_WIDTH }}>
+ <Popup
+ className="overview-analysis-graph-popup disabled-pointer-events"
+ placement={placement}
+ style={{ top, left, width: TOOLTIP_WIDTH }}>
<div className="overview-analysis-graph-tooltip">
<div className="overview-analysis-graph-tooltip-title">
<DateFormatter date={this.props.selectedDate} long={true} />
@@ -80,7 +80,7 @@ export default class PreviewGraphTooltips extends React.PureComponent {
</tbody>
</table>
</div>
- </BubblePopup>
+ </Popup>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
index 95ba6e24426..dc7782ee68b 100644
--- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
+++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
@@ -1,9 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<BubblePopup
- customClass="bubble-popup-right disabled-pointer-events"
- position={
+<Popup
+ className="overview-analysis-graph-popup disabled-pointer-events"
+ placement="left-top"
+ style={
Object {
"left": -135,
"top": 16,
@@ -47,5 +48,5 @@ exports[`should render correctly 1`] = `
</tbody>
</table>
</div>
-</BubblePopup>
+</Popup>
`;
diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
index 120f7cdeef5..fb4f5de12dc 100644
--- a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
+++ b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import BubblePopup, { BubblePopupPosition } from '../common/BubblePopup';
import MultiSelect from '../common/MultiSelect';
import { translate } from '../../helpers/l10n';
import './TagsList.css';
@@ -28,27 +27,22 @@ interface Props {
onSearch: (query: string) => Promise<void>;
onSelect: (item: string) => void;
onUnselect: (item: string) => void;
- position: BubblePopupPosition;
selectedTags: string[];
tags: string[];
}
export default function TagsSelector(props: Props) {
return (
- <BubblePopup
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
- position={props.position}>
- <MultiSelect
- elements={props.tags}
- listSize={props.listSize}
- onSearch={props.onSearch}
- onSelect={props.onSelect}
- onUnselect={props.onUnselect}
- placeholder={translate('search.search_for_tags')}
- selectedElements={props.selectedTags}
- validateSearchInput={validateTag}
- />
- </BubblePopup>
+ <MultiSelect
+ elements={props.tags}
+ listSize={props.listSize}
+ onSearch={props.onSearch}
+ onSelect={props.onSelect}
+ onUnselect={props.onUnselect}
+ placeholder={translate('search.search_for_tags')}
+ selectedElements={props.selectedTags}
+ validateSearchInput={validateTag}
+ />
);
}
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
index b6d33603ede..d3a553d60a8 100644
--- a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
@@ -1,61 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render with selected tags 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
- position={
- Object {
- "right": 0,
- "top": 0,
- }
+<MultiSelect
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
}
->
- <MultiSelect
- elements={
- Array [
- "foo",
- "bar",
- "baz",
- ]
- }
- filterSelected={[Function]}
- listSize={10}
- onSearch={[Function]}
- onSelect={[Function]}
- onUnselect={[Function]}
- placeholder="search.search_for_tags"
- renderLabel={[Function]}
- selectedElements={
- Array [
- "bar",
- ]
- }
- validateSearchInput={[Function]}
- />
-</BubblePopup>
+ filterSelected={[Function]}
+ listSize={10}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ placeholder="search.search_for_tags"
+ renderLabel={[Function]}
+ selectedElements={
+ Array [
+ "bar",
+ ]
+ }
+ validateSearchInput={[Function]}
+/>
`;
exports[`should render without tags at all 1`] = `
-<BubblePopup
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
- position={
- Object {
- "right": 0,
- "top": 0,
- }
- }
->
- <MultiSelect
- elements={Array []}
- filterSelected={[Function]}
- listSize={10}
- onSearch={[Function]}
- onSelect={[Function]}
- onUnselect={[Function]}
- placeholder="search.search_for_tags"
- renderLabel={[Function]}
- selectedElements={Array []}
- validateSearchInput={[Function]}
- />
-</BubblePopup>
+<MultiSelect
+ elements={Array []}
+ filterSelected={[Function]}
+ listSize={10}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ placeholder="search.search_for_tags"
+ renderLabel={[Function]}
+ selectedElements={Array []}
+ validateSearchInput={[Function]}
+/>
`;
diff --git a/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
index 5a5f7761e15..a78a573f41c 100644
--- a/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
+++ b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
@@ -30,7 +30,7 @@ interface Props {
export default function OrganizationListItem({ organization }: Props) {
return (
<li>
- <OrganizationLink className="dropdown-item-flex" organization={organization}>
+ <OrganizationLink className="display-flex-center" organization={organization}>
<div>
<OrganizationAvatar organization={organization} small={true} />
<span className="spacer-left">{organization.name}</span>
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
index bd3a76ad927..9e7c634c9dd 100644
--- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
@@ -3,7 +3,7 @@
exports[`renders 1`] = `
<li>
<OrganizationLink
- className="dropdown-item-flex"
+ className="display-flex-center"
organization={
Object {
"isAdmin": true,
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap
new file mode 100644
index 00000000000..750b6a3782e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/popups-test.tsx.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render Popup 1`] = `
+<div
+ className="popup is-left-top foo"
+ style={
+ Object {
+ "left": -5,
+ }
+ }
+>
+ <PopupArrow
+ style={
+ Object {
+ "top": -5,
+ }
+ }
+ />
+</div>
+`;
+
+exports[`should render PopupArrow 1`] = `
+<div
+ className="popup-arrow"
+ style={
+ Object {
+ "left": -5,
+ }
+ }
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx
new file mode 100644
index 00000000000..b108eed3601
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/popups-test.tsx
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { Popup, PopupPlacement, PopupArrow } from '../popups';
+
+it('should render Popup', () => {
+ expect(
+ shallow(
+ <Popup
+ arrowStyle={{ top: -5 }}
+ className="foo"
+ placement={PopupPlacement.LeftTop}
+ style={{ left: -5 }}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render PopupArrow', () => {
+ expect(shallow(<PopupArrow style={{ left: -5 }} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/ui/popups.css b/server/sonar-web/src/main/js/components/ui/popups.css
new file mode 100644
index 00000000000..efd2f6b9838
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/popups.css
@@ -0,0 +1,213 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.popup {
+ position: absolute;
+ z-index: var(--popupZIndex);
+ margin-top: -16px;
+ margin-left: 8px;
+ padding: var(--gridSize);
+ border: 1px solid var(--barBorderColor);
+ border-radius: 3px;
+ box-sizing: border-box;
+ background-color: #ffffff;
+ box-shadow: var(--defaultShadow);
+ cursor: default;
+}
+
+.popup.no-padding {
+ padding: 0;
+}
+
+/* #region .popup-arrow */
+.popup-arrow,
+.popup-arrow:after {
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border: 6px solid transparent;
+}
+
+.popup-arrow {
+ top: 15px;
+ left: -6px;
+ border-left-width: 0;
+ border-right-color: var(--barBorderColor);
+}
+
+.popup-arrow:after {
+ content: ' ';
+ left: 1px;
+ bottom: -6px;
+ border-left-width: 0;
+ border-right-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom */
+.popup.is-bottom {
+ top: 100%;
+ left: 0;
+ margin: 0;
+ margin-left: 50%;
+ transform: translate(-50%, 6px);
+}
+
+.popup.is-bottom .popup-arrow {
+ top: -6px;
+ left: calc(50% - 3px);
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom .popup-arrow.is-left {
+ left: 8px;
+}
+
+.popup.is-bottom .popup-arrow:after {
+ left: -6px;
+ bottom: -7px;
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom-right */
+.popup.is-bottom-right {
+ top: 100%;
+ right: 0;
+ margin: 0;
+ transform: translateY(6px);
+}
+
+.popup.is-bottom-right .popup-arrow {
+ top: -6px;
+ left: auto;
+ right: 8px;
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom-right .popup-arrow:after {
+ left: -6px;
+ bottom: -7px;
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom-left */
+.popup.is-bottom-left {
+ top: 100%;
+ left: 0;
+ margin: 0;
+ transform: translateY(6px);
+}
+
+.popup.is-bottom-left .popup-arrow {
+ top: -6px;
+ right: auto;
+ left: 8px;
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom-left .popup-arrow:after {
+ left: -6px;
+ bottom: -7px;
+ border-left-width: 6px;
+ border-top-width: 0;
+ border-right-color: transparent;
+ border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-left-top */
+.popup.is-left-top {
+ top: -4px;
+ right: 100%;
+ margin: 0;
+ transform: translateX(-6px);
+}
+
+.popup.is-left-top .popup-arrow {
+ right: -6px;
+ left: auto;
+ top: 8px;
+ border-right-width: 0;
+ border-left-width: 6px;
+ border-left-color: var(--barBorderColor);
+ border-right-color: transparent;
+}
+
+.popup.is-left-top .popup-arrow:after {
+ top: -6px;
+ left: -7px;
+ border-right-width: 0;
+ border-left-width: 6px;
+ border-left-color: #ffffff;
+ border-right-color: transparent;
+}
+/* #endregion */
+
+/* #region .popup.is-right-top */
+.popup.is-right-top {
+ top: -4px;
+ left: 100%;
+ margin: 0;
+ transform: translateX(6px);
+}
+
+.popup.is-right-top .popup-arrow {
+ left: -6px;
+ right: auto;
+ top: 8px;
+ border-left-width: 0;
+ border-right-width: 6px;
+ border-right-color: var(--barBorderColor);
+ border-left-color: transparent;
+}
+
+.popup.is-right-top .popup-arrow:after {
+ top: -6px;
+ right: -7px;
+ border-left-width: 0;
+ border-right-width: 6px;
+ border-right-color: #ffffff;
+ border-left-color: transparent;
+}
+/* #endregion */
+
+/* #region .popup & .menu or .multi-select */
+.popup:not(.no-padding) > .menu,
+.popup:not(.no-padding) > .multi-select {
+ margin: calc(-1 * var(--gridSize));
+}
+/* #endregion */
diff --git a/server/sonar-web/src/main/js/components/ui/popups.tsx b/server/sonar-web/src/main/js/components/ui/popups.tsx
new file mode 100644
index 00000000000..458f7e0b0ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/popups.tsx
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import './popups.css';
+
+export enum PopupPlacement {
+ Bottom = 'bottom',
+ BottomLeft = 'bottom-left',
+ BottomRight = 'bottom-right',
+ LeftTop = 'left-top',
+ RightTop = 'right-top'
+}
+
+interface PopupProps {
+ arrowStyle?: React.CSSProperties;
+ children?: React.ReactNode;
+ className?: string;
+ noPadding?: boolean;
+ placement?: PopupPlacement;
+ style?: React.CSSProperties;
+}
+
+export function Popup(props: PopupProps) {
+ const { placement = PopupPlacement.Bottom } = props;
+ return (
+ <div
+ className={classNames(
+ 'popup',
+ `is-${placement}`,
+ { 'no-padding': props.noPadding },
+ props.className
+ )}
+ style={props.style}>
+ {props.children}
+ <PopupArrow style={props.arrowStyle} />
+ </div>
+ );
+}
+
+interface PopupArrowProps {
+ style?: React.CSSProperties;
+}
+
+export function PopupArrow(props: PopupArrowProps) {
+ return <div className="popup-arrow" style={props.style} />;
+}
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts
index bdd52ec8e1e..d650182b932 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.ts
+++ b/server/sonar-web/src/main/js/helpers/testUtils.ts
@@ -78,6 +78,20 @@ export function elementKeydown(element: ShallowWrapper, keyCode: number): void {
}
}
+export function resizeWindowTo(width?: number, height?: number) {
+ // `document.body.clientWidth/clientHeight` are getters by default, so we need to redefine them
+ // pass `configurable: true` to allow to redefine the properties multiple times
+ if (width) {
+ Object.defineProperty(document.body, 'clientWidth', { configurable: true, value: width });
+ }
+ if (height) {
+ Object.defineProperty(document.body, 'clientHeight', { configurable: true, value: height });
+ }
+
+ const resizeEvent = new Event('resize');
+ window.dispatchEvent(resizeEvent);
+}
+
export function doAsync(fn?: Function): Promise<void> {
return new Promise(resolve => {
setImmediate(() => {