aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2017-04-03 17:56:23 +0200
committerStas Vilchik <stas-vilchik@users.noreply.github.com>2017-04-13 12:21:37 +0200
commit139261bbc13192621ef795d6d45298e1d8e1b7f3 (patch)
tree7aa153b4b3fec7e8fbf3b3b4f5ed0a1a5cc69113 /server/sonar-web/src
parentd665528c8751ead9ca93e3d18dd8600fac92834b (diff)
downloadsonarqube-139261bbc13192621ef795d6d45298e1d8e1b7f3.tar.gz
sonarqube-139261bbc13192621ef795d6d45298e1d8e1b7f3.zip
SONAR-9064 Rework facets sidebar on the issues page
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/components.js3
-rw-r--r--server/sonar-web/src/main/js/api/issues.js15
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js9
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap3
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js13
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js1
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/SearchView.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js9
-rw-r--r--server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js6
-rw-r--r--server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js6
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentName.js2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/controller.js1
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/init.js3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js1
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js1
-rw-r--r--server/sonar-web/src/main/js/apps/component-issues/init.js130
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js303
-rw-r--r--server/sonar-web/src/main/js/apps/issues/HeaderView.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/component-viewer/main.js132
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js649
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AppContainer.js (renamed from server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js)52
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js522
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js72
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js (renamed from server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js)50
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js106
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesList.js62
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ListItem.js106
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/PageActions.js77
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js122
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js49
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap48
-rw-r--r--server/sonar-web/src/main/js/apps/issues/controller.js217
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets-view.js63
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js135
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/author-facet.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/base-facet.js41
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/context-facet.js32
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js176
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js85
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/file-facet.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/language-facet.js84
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/module-facet.js46
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/project-facet.js112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js65
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js95
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/status-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js72
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/type-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/init.js87
-rw-r--r--server/sonar-web/src/main/js/apps/issues/issue-filter-view.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/layout.js62
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/issue.js30
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/issues.js108
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/state.js81
-rw-r--r--server/sonar-web/src/main/js/apps/issues/redirects.js55
-rw-r--r--server/sonar-web/src/main/js/apps/issues/router.js35
-rw-r--r--server/sonar-web/src/main/js/apps/issues/routes.js13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js168
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js99
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js276
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js120
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js67
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js116
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js114
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js113
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js160
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js119
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js126
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js (renamed from server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js)43
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js212
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js123
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js102
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js95
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js53
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap167
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js34
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js (renamed from server/sonar-web/src/main/js/components/issue/ConnectedIssue.js)33
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js83
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js71
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js (renamed from server/sonar-web/src/main/js/apps/component-issues/routes.js)24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js (renamed from server/sonar-web/src/main/js/components/issue/models/changelog.js)15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js (renamed from server/sonar-web/src/main/js/components/shared/qualifier-icon.js)13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js (renamed from server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js)11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap132
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap90
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css23
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs145
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs42
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs89
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs80
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.js229
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-header-view.js151
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js162
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-view.js125
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js2
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/projects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.js62
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js20
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js10
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js17
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js3
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js9
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js2
-rw-r--r--server/sonar-web/src/main/js/components/__tests__/issue-test.js197
-rw-r--r--server/sonar-web/src/main/js/components/charts/bar-chart.js20
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/EmptySearch.js39
-rw-r--r--server/sonar-web/src/main/js/components/common/MarkdownTips.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/SelectList.js77
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js6
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/components/common/action-options-view.js1
-rw-r--r--server/sonar-web/src/main/js/components/common/modals.js1
-rw-r--r--server/sonar-web/src/main/js/components/common/popup.js1
-rw-r--r--server/sonar-web/src/main/js/components/controls/Checkbox.js6
-rw-r--r--server/sonar-web/src/main/js/components/controls/DateInput.js6
-rw-r--r--server/sonar-web/src/main/js/components/issue/BaseIssue.js153
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.js145
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.js48
-rw-r--r--server/sonar-web/src/main/js/components/issue/actions.js60
-rw-r--r--server/sonar-web/src/main/js/components/issue/collections/issues.js101
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js22
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js32
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTransition.js12
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js69
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js7
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js3
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap55
-rw-r--r--server/sonar-web/src/main/js/components/issue/issue-view.js319
-rw-r--r--server/sonar-web/src/main/js/components/issue/models/issue.js281
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js137
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs6
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs18
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs5
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs10
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs37
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs13
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs11
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs11
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs17
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs10
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs12
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue.hbs182
-rw-r--r--server/sonar-web/src/main/js/components/issue/types.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js34
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/assign-form-view.js172
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/changelog-view.js36
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/comment-form-view.js113
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/issue-popup.js46
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/tags-form-view.js196
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js40
-rw-r--r--server/sonar-web/src/main/js/components/layout/Page.js (renamed from server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js)25
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageFilters.js (renamed from server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js)16
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageMain.js34
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageMainInner.js (renamed from server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js)21
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageSide.js73
-rw-r--r--server/sonar-web/src/main/js/components/navigator/workspace-list-view.js1
-rw-r--r--server/sonar-web/src/main/js/components/shared/Organization.js5
-rw-r--r--server/sonar-web/src/main/js/components/shared/QualifierIcon.js (renamed from server/sonar-web/src/main/js/apps/projects/components/NoProjects.js)25
-rw-r--r--server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js (renamed from server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js)29
-rw-r--r--server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap16
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/issues-test.js61
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/urls-test.js31
-rw-r--r--server/sonar-web/src/main/js/helpers/issues.js18
-rw-r--r--server/sonar-web/src/main/js/helpers/path.js9
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.js7
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.js25
-rw-r--r--server/sonar-web/src/main/js/libs/third-party/keymaster.js314
-rw-r--r--server/sonar-web/src/main/js/store/issues/duck.js50
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js4
-rw-r--r--server/sonar-web/src/main/less/components/issues.less6
-rw-r--r--server/sonar-web/src/main/less/components/page.less16
-rw-r--r--server/sonar-web/src/main/less/components/react-select.less38
-rw-r--r--server/sonar-web/src/main/less/components/search-navigator.less24
-rw-r--r--server/sonar-web/src/main/less/pages/issues.less29
235 files changed, 6724 insertions, 6708 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 512f3a1f7ef..c9bea5e039d 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -137,6 +137,9 @@ export function searchProjects(data?: Object) {
return getJSON(url, data);
}
+export const searchComponents = (data?: { q?: string, qualifiers?: string, ps?: number }) =>
+ getJSON('/api/components/search', data);
+
/**
* Change component's key
* @param {string} from
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js
index 912f8b2f6d3..ae316d8c228 100644
--- a/server/sonar-web/src/main/js/api/issues.js
+++ b/server/sonar-web/src/main/js/api/issues.js
@@ -41,15 +41,6 @@ type IssuesResponse = {
users?: Array<*>
};
-export type Transition =
- | 'confirm'
- | 'unconfirm'
- | 'reopen'
- | 'resolve'
- | 'falsepositive'
- | 'wontfix'
- | 'close';
-
export const searchIssues = (query: {}): Promise<IssuesResponse> =>
getJSON('/api/issues/search', query);
@@ -97,7 +88,9 @@ export function getIssuesCount(query: {}): Promise<*> {
});
}
-export const searchIssueTags = (ps: number = 500) => getJSON('/api/issues/tags', { ps });
+export const searchIssueTags = (
+ data: { ps?: number, q?: string } = { ps: 500 }
+): Promise<Array<string>> => getJSON('/api/issues/tags', data).then(r => r.tags);
export function getIssueChangelog(issue: string): Promise<*> {
const url = '/api/issues/changelog';
@@ -142,7 +135,7 @@ export function setIssueTags(data: { issue: string, tags: string }): Promise<Iss
}
export function setIssueTransition(
- data: { issue: string, transition: Transition }
+ data: { issue: string, transition: string }
): Promise<IssueResponse> {
const url = '/api/issues/do_transition';
return postJSON(url, data);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
index 2f668b0b8b9..0d4200d6bf5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
@@ -20,7 +20,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
-import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
import OrganizationLink from '../../../../components/ui/OrganizationLink';
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
index 6b5ba3661a8..6c68203c03b 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
@@ -101,11 +101,14 @@ export default class ComponentNavMenu extends React.Component {
);
}
- renderComponentIssuesLink() {
+ renderIssuesLink() {
return (
<li>
<Link
- to={{ pathname: '/component_issues', query: { id: this.props.component.key } }}
+ to={{
+ pathname: '/project/issues',
+ query: { id: this.props.component.key, resolved: 'false' }
+ }}
activeClassName="active">
{translate('issues.page')}
</Link>
@@ -343,7 +346,7 @@ export default class ComponentNavMenu extends React.Component {
return (
<ul className="nav navbar-nav nav-tabs">
{this.renderDashboardLink()}
- {this.renderComponentIssuesLink()}
+ {this.renderIssuesLink()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
{this.renderActivityLink()}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
index d9bd895c7b1..810d438e08e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
@@ -4,7 +4,7 @@ exports[`test should not render breadcrumbs with one element 1`] = `
<span>
<span
className="navbar-context-title-qualifier little-spacer-right">
- <qualifier-icon
+ <QualifierIcon
qualifier="TRK" />
</span>
<Link
@@ -33,7 +33,7 @@ exports[`test should render organization 1`] = `
<span>
<span
className="navbar-context-title-qualifier little-spacer-right">
- <qualifier-icon
+ <QualifierIcon
qualifier="TRK" />
</span>
<OrganizationLink
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
index d6af75c3865..898c8de040f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
@@ -25,9 +25,10 @@ exports[`test should work with extensions 1`] = `
style={Object {}}
to={
Object {
- "pathname": "/component_issues",
+ "pathname": "/project/issues",
"query": Object {
"id": "foo",
+ "resolved": "false",
},
}
}>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
index 1c7f395f741..3a5f67cdada 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
@@ -25,7 +25,10 @@ import { isUserAdmin } from '../../../../helpers/users';
export default class GlobalNavMenu extends React.Component {
static propTypes = {
appState: React.PropTypes.object.isRequired,
- currentUser: React.PropTypes.object.isRequired
+ currentUser: React.PropTypes.object.isRequired,
+ location: React.PropTypes.shape({
+ pathname: React.PropTypes.string.isRequired
+ }).isRequired
};
static defaultProps = {
@@ -59,12 +62,12 @@ export default class GlobalNavMenu extends React.Component {
renderIssuesLink() {
const query = this.props.currentUser.isLoggedIn
- ? '#resolved=false|assigned_to_me=true'
- : '#resolved=false';
- const url = '/issues' + query;
+ ? { myIssues: 'true', resolved: 'false' }
+ : { resolved: 'false' };
+ const active = this.props.location.pathname === 'issues';
return (
<li>
- <Link to={url} className={this.activeLink('/issues')}>
+ <Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}>
{translate('issues.page')}
</Link>
</li>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
index cb8349ecca5..d7089b5a536 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
@@ -20,6 +20,7 @@
import Backbone from 'backbone';
import React from 'react';
import { connect } from 'react-redux';
+import key from 'keymaster';
import SearchView from './SearchView';
import { getCurrentUser } from '../../../../store/rootReducer';
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
index 163ec964e26..9277cb5acb5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
@@ -253,7 +253,7 @@ export default Marionette.LayoutView.extend({
getNavigationFindings(q) {
const DEFAULT_ITEMS = [
- { name: translate('issues.page'), url: window.baseUrl + '/issues/search' },
+ { name: translate('issues.page'), url: window.baseUrl + '/issues' },
{
name: translate('layout.measures'),
url: window.baseUrl + '/measures/search?qualifiers[]=TRK'
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
index d21f3f609ee..99b97c82c4f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
@@ -30,6 +30,8 @@ it('should work with extensions', () => {
isLoggedIn: false,
permissions: { global: [] }
};
- const wrapper = shallow(<GlobalNavMenu appState={appState} currentUser={currentUser} />);
+ const wrapper = shallow(
+ <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
+ );
expect(wrapper).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
index 2f2dc6dd0f3..30ec923b76a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
@@ -12,10 +12,16 @@ exports[`test should work with extensions 1`] = `
</li>
<li>
<Link
- className={null}
onlyActiveOnIndex={false}
style={Object {}}
- to="/issues#resolved=false">
+ to={
+ Object {
+ "pathname": "/issues",
+ "query": Object {
+ "resolved": "false",
+ },
+ }
+ }>
issues.page
</Link>
</li>
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 34f86b21085..932192fabc3 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -44,7 +44,6 @@ import backgroundTasksRoutes from '../../apps/background-tasks/routes';
import codeRoutes from '../../apps/code/routes';
import codingRulesRoutes from '../../apps/coding-rules/routes';
import componentRoutes from '../../apps/component/routes';
-import componentIssuesRoutes from '../../apps/component-issues/routes';
import componentMeasuresRoutes from '../../apps/component-measures/routes';
import customMeasuresRoutes from '../../apps/custom-measures/routes';
import groupsRoutes from '../../apps/groups/routes';
@@ -89,9 +88,8 @@ const startReactApp = () => {
<Router history={history} onUpdate={handleUpdate}>
<Route
path="/account/issues"
- onEnter={() => {
- const defaultFilter = window.location.hash || '#resolve=false';
- window.location = `${window.baseUrl}/issues${defaultFilter}|assigned_to_me=true`;
+ onEnter={(_, replace) => {
+ replace({ pathname: '/issues', query: { myIssues: 'true', resolved: 'false' } });
}}
/>
@@ -117,6 +115,7 @@ const startReactApp = () => {
/>
<Redirect from="/component/index" to="/component" />
+ <Redirect from="/component_issues" to="/project/issues" />
<Redirect from="/dashboard/index" to="/dashboard" />
<Redirect from="/governance" to="/view" />
<Redirect from="/extension/governance/portfolios" to="/portfolios" />
@@ -158,7 +157,6 @@ const startReactApp = () => {
<Route component={ProjectContainer}>
<Route path="code" childRoutes={codeRoutes} />
- <Route path="component_issues" childRoutes={componentIssuesRoutes} />
<Route path="component_measures" childRoutes={componentMeasuresRoutes} />
<Route path="custom_measures" childRoutes={customMeasuresRoutes} />
<Route path="dashboard" childRoutes={overviewRoutes} />
@@ -176,6 +174,7 @@ const startReactApp = () => {
component={ProjectPageExtension}
/>
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
+ <Route path="issues" childRoutes={issuesRoutes} />
<Route path="settings" childRoutes={settingsRoutes} />
{projectAdminRoutes}
</Route>
diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
index 3ae7d82992d..fd231aa512b 100644
--- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
+++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
@@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'BUG' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
index 3b974ffd770..11db35cac3b 100644
--- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
+++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
@@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'BUG' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
index 6c7d13c15b7..2a9fb4157ae 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import TaskType from './TaskType';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import Organization from '../../../components/shared/Organization';
import { Task } from '../types';
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
index 9ad27ba80ec..76ea80e652f 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
@@ -20,7 +20,7 @@
import React from 'react';
import { Link } from 'react-router';
import Truncated from './Truncated';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
function getTooltip(component) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/controller.js b/server/sonar-web/src/main/js/apps/coding-rules/controller.js
index 01943b685bf..dd15fb39b92 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/controller.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/controller.js
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
+import key from 'keymaster';
import Controller from '../../components/navigator/controller';
import Rule from './models/rule';
import RuleDetailsView from './rule-details-view';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js
index 57e689c5178..437505c5eaf 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/init.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/init.js
@@ -22,6 +22,7 @@ import $ from 'jquery';
import { sortBy } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
import State from './models/state';
import Layout from './layout';
import Rules from './models/rules';
@@ -105,7 +106,7 @@ App.on('start', function(options: {
});
this.layout.filtersRegion.show(this.filtersView);
- window.key.setScope('list');
+ key.setScope('list');
this.router = new Router({
app: this
});
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
index 307765e54c6..722e3f22d3e 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
@@ -21,6 +21,7 @@ import $ from 'jquery';
import { union } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
import Rules from './models/rules';
import MetaView from './rule/rule-meta-view';
import DescView from './rule/rule-description-view';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
index 1e32a633a9a..b218d6939b9 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import key from 'keymaster';
import WorkspaceListView from '../../components/navigator/workspace-list-view';
import WorkspaceListItemView from './workspace-list-item-view';
import WorkspaceListEmptyView from './workspace-list-empty-view';
diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js
deleted file mode 100644
index 4b5abd4eb72..00000000000
--- a/server/sonar-web/src/main/js/apps/component-issues/init.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import { difference } from 'lodash';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import State from '../issues/models/state';
-import Layout from '../issues/layout';
-import Issues from '../issues/models/issues';
-import Facets from '../../components/navigator/models/facets';
-import Controller from '../issues/controller';
-import Router from '../issues/router';
-import WorkspaceListView from '../issues/workspace-list-view';
-import WorkspaceHeaderView from '../issues/workspace-header-view';
-import FacetsView from './../issues/facets-view';
-import HeaderView from './../issues/HeaderView';
-
-const App = new Marionette.Application();
-const init = function({ el, component, currentUser }) {
- this.config = {
- resource: component.id,
- resourceName: component.name,
- resourceQualifier: component.qualifier
- };
- this.state = new State({
- canBulkChange: currentUser.isLoggedIn,
- isContext: true,
- contextQuery: { componentUuids: this.config.resource },
- contextComponentUuid: this.config.resource,
- contextComponentName: this.config.resourceName,
- contextComponentQualifier: this.config.resourceQualifier,
- contextOrganization: component.organization
- });
- this.updateContextFacets();
- this.list = new Issues();
- this.facets = new Facets();
-
- this.layout = new Layout({ app: this, el });
- this.layout.render();
- $('#footer').addClass('search-navigator-footer');
-
- this.controller = new Controller({ app: this });
-
- this.issuesView = new WorkspaceListView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceListRegion.show(this.issuesView);
- this.issuesView.bindScrollEvents();
-
- this.workspaceHeaderView = new WorkspaceHeaderView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
-
- this.facetsView = new FacetsView({
- app: this,
- collection: this.facets
- });
- this.layout.facetsRegion.show(this.facetsView);
-
- this.headerView = new HeaderView({
- app: this
- });
- this.layout.filtersRegion.show(this.headerView);
-
- key.setScope('list');
- App.router = new Router({ app: App });
- Backbone.history.start();
-};
-
-App.getContextQuery = function() {
- return { componentUuids: this.config.resource };
-};
-
-App.getRestrictedFacets = function() {
- return {
- TRK: ['projectUuids'],
- BRC: ['projectUuids'],
- DIR: ['projectUuids', 'moduleUuids', 'directories'],
- DEV: ['authors'],
- DEV_PRJ: ['projectUuids', 'authors']
- };
-};
-
-App.updateContextFacets = function() {
- const facets = this.state.get('facets');
- const allFacets = this.state.get('allFacets');
- const facetsFromServer = this.state.get('facetsFromServer');
- return this.state.set({
- facets,
- allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]),
- facetsFromServer: difference(
- facetsFromServer,
- this.getRestrictedFacets()[this.config.resourceQualifier]
- )
- });
-};
-
-App.on('start', options => {
- init.call(App, options);
-});
-
-export default function(el, component, currentUser) {
- App.start({ el, component, currentUser });
-
- return () => {
- Backbone.history.stop();
- App.layout.destroy();
- $('#footer').removeClass('search-navigator-footer');
- };
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
index 03d857462bf..5261badeaf2 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { isDiffMetric, formatLeak } from '../../utils';
import { formatMeasure } from '../../../../helpers/measures';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
index 1ccdd7eed1b..43d0d8bd422 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import classNames from 'classnames';
-import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { splitPath } from '../../../../helpers/path';
import { getComponentUrl } from '../../../../helpers/urls';
diff --git a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js
deleted file mode 100644
index 1c9b447b231..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import { debounce, sortBy } from 'lodash';
-import ModalForm from '../../components/common/modal-form';
-import Template from './templates/BulkChangeForm.hbs';
-import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
-import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues';
-import { searchUsers } from '../../api/users';
-import { searchMembers } from '../../api/organizations';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-
-const LIMIT = 500;
-const INPUT_WIDTH = '250px';
-const MINIMUM_QUERY_LENGTH = 2;
-const UNASSIGNED = '<UNASSIGNED>';
-
-type Issue = {
- actions?: Array<string>,
- assignee: string | null,
- transitions?: Array<string>
-};
-
-const hasAction = (action: string) =>
- (issue: Issue) => issue.actions && issue.actions.includes(action);
-
-export default ModalForm.extend({
- template: Template,
-
- initialize() {
- this.issues = null;
- this.paging = null;
- this.tags = null;
- this.loadIssues();
- this.loadTags();
- },
-
- loadIssues() {
- const { query } = this.options;
- searchIssues({
- ...query,
- additionalFields: 'actions,transitions',
- ps: LIMIT
- }).then(r => {
- this.issues = r.issues;
- this.paging = r.paging;
- this.render();
- });
- },
-
- loadTags() {
- searchIssueTags().then(r => {
- this.tags = r.tags;
- this.render();
- });
- },
-
- assigneeSearch(defaultOptions) {
- const { context } = this.options;
- return debounce(
- query => {
- if (query.term.length === 0) {
- query.callback({ results: defaultOptions });
- } else if (query.term.length >= MINIMUM_QUERY_LENGTH) {
- const onSuccess = r => {
- query.callback({
- results: r.users.map(user => ({
- id: user.login,
- text: `${user.name} (${user.login})`
- }))
- });
- };
- if (context.isContext) {
- searchMembers({ organization: context.organization, q: query.term }).then(onSuccess);
- } else {
- searchUsers(query.term).then(onSuccess);
- }
- }
- },
- 250
- );
- },
-
- prepareAssigneeSelect() {
- const input = this.$('#assignee');
- if (input.length) {
- const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues);
- const currentUser = getCurrentUserFromStore();
- const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues);
- const defaultOptions = [];
- if (canBeAssignedToMe && currentUser.isLoggedIn) {
- defaultOptions.push({
- id: currentUser.login,
- text: `${currentUser.name} (${currentUser.login})`
- });
- }
- if (canBeUnassigned) {
- defaultOptions.push({ id: UNASSIGNED, text: translate('unassigned') });
- }
-
- input.select2({
- allowClear: false,
- placeholder: translate('search_verb'),
- width: INPUT_WIDTH,
- formatNoMatches: () => translate('select2.noMatches'),
- formatSearching: () => translate('select2.searching'),
- formatInputTooShort: () =>
- translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH),
- query: this.assigneeSearch(defaultOptions)
- });
-
- input.on('change', () => this.$('#assign-action').prop('checked', true));
- }
- },
-
- prepareTypeSelect() {
- this.$('#type')
- .select2({
- minimumResultsForSearch: 999,
- width: INPUT_WIDTH
- })
- .on('change', () => this.$('#set-type-action').prop('checked', true));
- },
-
- prepareSeveritySelect() {
- const format = state =>
- state.id
- ? `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}`
- : state.text;
- this.$('#severity')
- .select2({
- minimumResultsForSearch: 999,
- width: INPUT_WIDTH,
- formatResult: format,
- formatSelection: format
- })
- .on('change', () => this.$('#set-severity-action').prop('checked', true));
- },
-
- prepareTagsInput() {
- this.$('#add_tags')
- .select2({
- width: INPUT_WIDTH,
- tags: this.tags
- })
- .on('change', () => this.$('#add-tags-action').prop('checked', true));
-
- this.$('#remove_tags')
- .select2({
- width: INPUT_WIDTH,
- tags: this.tags
- })
- .on('change', () => this.$('#remove-tags-action').prop('checked', true));
- },
-
- onRender() {
- ModalForm.prototype.onRender.apply(this, arguments);
- this.prepareAssigneeSelect();
- this.prepareTypeSelect();
- this.prepareSeveritySelect();
- this.prepareTagsInput();
- },
-
- onFormSubmit() {
- ModalForm.prototype.onFormSubmit.apply(this, arguments);
- const query = {};
-
- const assignee = this.$('#assignee').val();
- if (this.$('#assign-action').is(':checked') && assignee != null) {
- query['assign'] = assignee === UNASSIGNED ? '' : assignee;
- }
-
- const type = this.$('#type').val();
- if (this.$('#set-type-action').is(':checked') && type) {
- query['set_type'] = type;
- }
-
- const severity = this.$('#severity').val();
- if (this.$('#set-severity-action').is(':checked') && severity) {
- query['set_severity'] = severity;
- }
-
- const addedTags = this.$('#add_tags').val();
- if (this.$('#add-tags-action').is(':checked') && addedTags) {
- query['add_tags'] = addedTags;
- }
-
- const removedTags = this.$('#remove_tags').val();
- if (this.$('#remove-tags-action').is(':checked') && removedTags) {
- query['remove_tags'] = removedTags;
- }
-
- const transition = this.$('[name="do_transition.transition"]:checked').val();
- if (transition) {
- query['do_transition'] = transition;
- }
-
- const comment = this.$('#comment').val();
- if (comment) {
- query['comment'] = comment;
- }
-
- const sendNotifications = this.$('#send-notifications').is(':checked');
- if (sendNotifications) {
- query['sendNotifications'] = sendNotifications;
- }
-
- this.disableForm();
- this.showSpinner();
-
- const issueKeys = this.issues.map(issue => issue.key);
- bulkChangeIssues(issueKeys, query).then(
- () => {
- this.destroy();
- this.options.onChange();
- },
- (e: Object) => {
- this.enableForm();
- this.hideSpinner();
- e.response.json().then(r => this.showErrors(r.errors, r.warnings));
- }
- );
- },
-
- canBeAssigned(issues: Array<Issue>) {
- return issues.filter(hasAction('assign')).length;
- },
-
- canBeAssignedToMe(issues: Array<Issue>) {
- return issues.filter(hasAction('assign_to_me')).length;
- },
-
- canBeUnassigned(issues: Array<Issue>) {
- return issues.filter(issue => issue.assignee).length;
- },
-
- canChangeType(issues: Array<Issue>) {
- return issues.filter(hasAction('set_type')).length;
- },
-
- canChangeSeverity(issues: Array<Issue>) {
- return issues.filter(hasAction('set_severity')).length;
- },
-
- canChangeTags(issues: Array<Issue>) {
- return issues.filter(hasAction('set_tags')).length;
- },
-
- canBeCommented(issues: Array<Issue>) {
- return issues.filter(hasAction('comment')).length;
- },
-
- availableTransitions(issues: Array<Issue>) {
- const transitions = {};
- issues.forEach(issue => {
- if (issue.transitions) {
- issue.transitions.forEach(t => {
- if (transitions[t] != null) {
- transitions[t]++;
- } else {
- transitions[t] = 1;
- }
- });
- }
- });
- return sortBy(Object.keys(transitions)).map(transition => ({
- transition,
- count: transitions[transition]
- }));
- },
-
- serializeData() {
- return {
- ...ModalForm.prototype.serializeData.apply(this, arguments),
- isLoaded: this.issues != null && this.tags != null,
- issues: this.issues,
- limitReached: this.paging && this.paging.total > LIMIT,
- canBeAssigned: this.issues && this.canBeAssigned(this.issues),
- canChangeType: this.issues && this.canChangeType(this.issues),
- canChangeSeverity: this.issues && this.canChangeSeverity(this.issues),
- canChangeTags: this.issues && this.canChangeTags(this.issues),
- canBeCommented: this.issues && this.canBeCommented(this.issues),
- availableTransitions: this.issues && this.availableTransitions(this.issues)
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/HeaderView.js b/server/sonar-web/src/main/js/apps/issues/HeaderView.js
deleted file mode 100644
index 596996f9d5c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/HeaderView.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/facets/issues-my-issues-facet.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
- className: 'issues-header-inner',
-
- events: {
- 'change [name="issues-page-my"]': 'onMyIssuesChange'
- },
-
- initialize() {
- this.listenTo(this.options.app.state, 'change:query', this.render);
- },
-
- onMyIssuesChange() {
- const mode = this.$('[name="issues-page-my"]:checked').val();
- if (mode === 'my') {
- this.options.app.state.updateFilter({
- assigned_to_me: 'true',
- assignees: null,
- assigned: null
- });
- } else {
- this.options.app.state.updateFilter({
- assigned_to_me: null,
- assignees: null,
- assigned: null
- });
- }
- },
- serializeData() {
- const me = !!this.options.app.state.get('query').assigned_to_me;
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- me,
- isContext: this.options.app.state.get('isContext'),
- user: this.options.app.state.get('user')
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
deleted file mode 100644
index a1dcb64d7dc..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import React from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-import Marionette from 'backbone.marionette';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import WithStore from '../../../components/shared/WithStore';
-
-export default Marionette.ItemView.extend({
- template() {
- return '<div></div>';
- },
-
- initialize(options) {
- this.handleLoadIssues = this.handleLoadIssues.bind(this);
- this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this);
- this.selectIssue = this.selectIssue.bind(this);
- this.listenTo(options.app.state, 'change:selectedIndex', this.select);
- },
-
- onRender() {
- this.showViewer();
- },
-
- onDestroy() {
- this.unbindShortcuts();
- unmountComponentAtNode(this.el);
- },
-
- handleLoadIssues(component: string) {
- // TODO fromLine: number, toLine: number
- const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component);
- return Promise.resolve(issues);
- },
-
- showViewer(onLoaded) {
- if (!this.baseIssue) {
- return;
- }
-
- const componentKey = this.baseIssue.get('component');
-
- render(
- <WithStore>
- <SourceViewer
- aroundLine={this.baseIssue.get('line')}
- component={componentKey}
- displayAllIssues={true}
- loadIssues={this.handleLoadIssues}
- onLoaded={onLoaded}
- onIssueSelect={this.selectIssue}
- selectedIssue={this.baseIssue.get('key')}
- />
- </WithStore>,
- this.el
- );
- },
-
- openFileByIssue(issue) {
- this.baseIssue = issue;
- this.selectedIssue = issue.get('key');
- this.showViewer(this.scrollToBaseIssue);
- this.bindShortcuts();
- },
-
- bindShortcuts() {
- key('up', 'componentViewer', () => {
- this.options.app.controller.selectPrev();
- return false;
- });
- key('down', 'componentViewer', () => {
- this.options.app.controller.selectNext();
- return false;
- });
- key('left,backspace', 'componentViewer', () => {
- this.options.app.controller.closeComponentViewer();
- return false;
- });
- },
-
- unbindShortcuts() {
- key.deleteScope('componentViewer');
- },
-
- select() {
- const selected = this.options.app.state.get('selectedIndex');
- const selectedIssue = this.options.app.list.at(selected);
-
- if (selectedIssue.get('component') === this.baseIssue.get('component')) {
- this.baseIssue = selectedIssue;
- this.showViewer(this.scrollToBaseIssue);
- this.scrollToBaseIssue();
- } else {
- this.options.app.controller.showComponentViewer(selectedIssue);
- }
- },
-
- scrollToLine(line) {
- const row = this.$(`[data-line-number=${line}]`);
- const topOffset = $(window).height() / 2 - 60;
- const goal = row.length > 0 ? row.offset().top - topOffset : 0;
- $(window).scrollTop(goal);
- },
-
- selectIssue(issueKey) {
- const issue = this.options.app.list.find(model => model.get('key') === issueKey);
- const index = this.options.app.list.indexOf(issue);
- this.options.app.state.set({ selectedIndex: index });
- },
-
- scrollToBaseIssue() {
- this.scrollToLine(this.baseIssue.get('line'));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
new file mode 100644
index 00000000000..73574c611b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -0,0 +1,649 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Helmet from 'react-helmet';
+import key from 'keymaster';
+import { keyBy, without } from 'lodash';
+import HeaderPanel from './HeaderPanel';
+import PageActions from './PageActions';
+import FiltersHeader from './FiltersHeader';
+import MyIssuesFilter from './MyIssuesFilter';
+import Sidebar from '../sidebar/Sidebar';
+import IssuesList from './IssuesList';
+import ComponentBreadcrumbs from './ComponentBreadcrumbs';
+import IssuesSourceViewer from './IssuesSourceViewer';
+import BulkChangeModal from './BulkChangeModal';
+import {
+ parseQuery,
+ areMyIssuesSelected,
+ areQueriesEqual,
+ getOpen,
+ serializeQuery,
+ parseFacets
+} from '../utils';
+import type {
+ Query,
+ Paging,
+ Facet,
+ ReferencedComponent,
+ ReferencedUser,
+ ReferencedLanguage,
+ Component,
+ CurrentUser
+} from '../utils';
+import ListFooter from '../../../components/controls/ListFooter';
+import EmptySearch from '../../../components/common/EmptySearch';
+import Page from '../../../components/layout/Page';
+import PageMain from '../../../components/layout/PageMain';
+import PageMainInner from '../../../components/layout/PageMainInner';
+import PageSide from '../../../components/layout/PageSide';
+import PageFilters from '../../../components/layout/PageFilters';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { scrollToElement } from '../../../helpers/scrolling';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {
+ component?: Component,
+ currentUser: CurrentUser,
+ fetchIssues: () => Promise<*>,
+ location: { pathname: string, query: { [string]: string } },
+ onRequestFail: (Error) => void,
+ router: { push: () => void, replace: () => void }
+};
+
+type State = {
+ bulkChange: 'all' | 'selected' | null,
+ checked: Array<string>,
+ facets: { [string]: Facet },
+ issues: Array<Issue>,
+ loading: boolean,
+ myIssues: boolean,
+ openFacets: { [string]: boolean },
+ paging?: Paging,
+ query: Query,
+ referencedComponents: { [string]: ReferencedComponent },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ referencedRules: { [string]: { name: string } },
+ referencedUsers: { [string]: ReferencedUser },
+ selected?: string
+};
+
+const DEFAULT_QUERY = { resolved: 'false' };
+
+export default class App extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ bulkChange: null,
+ checked: [],
+ facets: {},
+ issues: [],
+ loading: true,
+ myIssues: areMyIssuesSelected(props.location.query),
+ openFacets: { resolutions: true, types: true },
+ query: parseQuery(props.location.query),
+ referencedComponents: {},
+ referencedLanguages: {},
+ referencedRules: {},
+ referencedUsers: {},
+ selected: getOpen(props.location.query)
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+
+ const footer = document.getElementById('footer');
+ if (footer) {
+ footer.classList.add('search-navigator-footer');
+ }
+
+ this.attachShortcuts();
+ this.fetchFirstIssues();
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ const open = getOpen(nextProps.location.query);
+ if (open != null && open !== this.state.selected) {
+ this.setState({ selected: open });
+ }
+ this.setState({
+ myIssues: areMyIssuesSelected(nextProps.location.query),
+ query: parseQuery(nextProps.location.query)
+ });
+ }
+
+ componentDidUpdate(prevProps: Props, prevState: State) {
+ const { query } = this.props.location;
+ const { query: prevQuery } = prevProps.location;
+ if (
+ !areQueriesEqual(prevQuery, query) ||
+ areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
+ ) {
+ this.fetchFirstIssues();
+ } else if (prevState.selected !== this.state.selected) {
+ const open = getOpen(query);
+ if (!open) {
+ this.scrollToSelectedIssue();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.detachShortcuts();
+
+ const footer = document.getElementById('footer');
+ if (footer) {
+ footer.classList.remove('search-navigator-footer');
+ }
+
+ this.mounted = false;
+ }
+
+ attachShortcuts() {
+ key.setScope('issues');
+ key('up', 'issues', () => {
+ this.selectPreviousIssue();
+ return false;
+ });
+ key('down', 'issues', () => {
+ this.selectNextIssue();
+ return false;
+ });
+ key('right', 'issues', () => {
+ this.openSelectedIssue();
+ return false;
+ });
+ key('left', 'issues', () => {
+ this.closeIssue();
+ return false;
+ });
+ }
+
+ detachShortcuts() {
+ key.deleteScope('issues');
+ }
+
+ getSelectedIndex(): ?number {
+ const { issues, selected } = this.state;
+ const index = issues.findIndex(issue => issue.key === selected);
+ return index !== -1 ? index : null;
+ }
+
+ selectNextIssue = () => {
+ const { issues } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (issues != null && selectedIndex != null && selectedIndex < issues.length - 1) {
+ if (getOpen(this.props.location.query)) {
+ this.openIssue(issues[selectedIndex + 1].key);
+ } else {
+ this.setState({ selected: issues[selectedIndex + 1].key });
+ }
+ }
+ };
+
+ selectPreviousIssue = () => {
+ const { issues } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (issues != null && selectedIndex != null && selectedIndex > 0) {
+ if (getOpen(this.props.location.query)) {
+ this.openIssue(issues[selectedIndex - 1].key);
+ } else {
+ this.setState({ selected: issues[selectedIndex - 1].key });
+ }
+ }
+ };
+
+ openIssue = (issue: string) => {
+ const path = {
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(this.state.query),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined,
+ open: issue
+ }
+ };
+ const open = getOpen(this.props.location.query);
+ if (open) {
+ this.props.router.replace(path);
+ } else {
+ this.props.router.push(path);
+ }
+ };
+
+ closeIssue = () => {
+ if (this.state.query) {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(this.state.query),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined,
+ open: undefined
+ }
+ });
+ }
+ };
+
+ openSelectedIssue = () => {
+ const { selected } = this.state;
+ if (selected) {
+ this.openIssue(selected);
+ }
+ };
+
+ scrollToSelectedIssue = () => {
+ const { selected } = this.state;
+ if (selected) {
+ const element = document.querySelector(`[data-issue="${selected}"]`);
+ if (element) {
+ scrollToElement(element, 150, 100);
+ }
+ }
+ };
+
+ fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
+ const { component } = this.props;
+ const { myIssues, query } = this.state;
+
+ const parameters = {
+ componentKeys: component && component.key,
+ ...serializeQuery(query),
+ s: 'FILE_LINE',
+ ps: 25,
+ facets: requestFacets
+ ? [
+ 'assignees',
+ 'authors',
+ 'createdAt',
+ 'directories',
+ 'fileUuids',
+ 'languages',
+ 'moduleUuids',
+ 'projectUuids',
+ 'resolutions',
+ 'rules',
+ 'severities',
+ 'statuses',
+ 'tags',
+ 'types'
+ ].join()
+ : undefined,
+ ...additional
+ };
+
+ if (myIssues) {
+ Object.assign(parameters, { assignees: '__me__' });
+ }
+
+ return this.props.fetchIssues(parameters);
+ };
+
+ fetchFirstIssues() {
+ this.setState({ loading: true });
+ this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => {
+ if (this.mounted) {
+ const open = getOpen(this.props.location.query);
+ this.setState({
+ facets: parseFacets(facets),
+ loading: false,
+ issues,
+ paging,
+ referencedComponents: keyBy(other.components, 'uuid'),
+ referencedLanguages: keyBy(other.languages, 'key'),
+ referencedRules: keyBy(other.rules, 'key'),
+ referencedUsers: keyBy(other.users, 'login'),
+ selected: issues.length > 0
+ ? issues.find(issue => issue.key === open) != null ? open : issues[0].key
+ : undefined
+ });
+ }
+ });
+ }
+
+ fetchIssuesPage = (p: number): Promise<*> => {
+ return this.fetchIssues({ p });
+ };
+
+ fetchIssuesUntil = (p: number, done: (Array<Issue>, Paging) => boolean) => {
+ return this.fetchIssuesPage(p).then(response => {
+ const { issues, paging } = response;
+
+ return done(issues, paging)
+ ? { issues, paging }
+ : this.fetchIssuesUntil(p + 1, done).then(nextResponse => {
+ return {
+ issues: [...issues, ...nextResponse.issues],
+ paging: nextResponse.paging
+ };
+ });
+ });
+ };
+
+ fetchMoreIssues = () => {
+ const { paging } = this.state;
+
+ if (!paging) {
+ return;
+ }
+
+ const p = paging.pageIndex + 1;
+
+ this.setState({ loading: true });
+ this.fetchIssuesPage(p).then(response => {
+ if (this.mounted) {
+ this.setState(state => ({
+ loading: false,
+ issues: [...state.issues, ...response.issues],
+ paging: response.paging
+ }));
+ }
+ });
+ };
+
+ fetchIssuesForComponent = (): Promise<Array<Issue>> => {
+ const { issues, paging } = this.state;
+
+ const open = getOpen(this.props.location.query);
+ const openIssue = issues.find(issue => issue.key === open);
+
+ if (!openIssue || !paging) {
+ return Promise.reject();
+ }
+
+ const isSameComponent = (issue: Issue): boolean => issue.component === openIssue.component;
+
+ const done = (issues: Array<Issue>, paging: Paging): boolean =>
+ paging.total <= paging.pageIndex * paging.pageSize ||
+ issues[issues.length - 1].component !== openIssue.component;
+
+ if (done(issues, paging)) {
+ return Promise.resolve(issues.filter(isSameComponent));
+ }
+
+ this.setState({ loading: true });
+ return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(response => {
+ const nextIssues = [...issues, ...response.issues];
+
+ this.setState({
+ issues: nextIssues,
+ loading: false,
+ paging: response.paging
+ });
+ return nextIssues.filter(isSameComponent);
+ });
+ };
+
+ isFiltered = () => {
+ const serialized = serializeQuery(this.state.query);
+ return !areQueriesEqual(serialized, DEFAULT_QUERY);
+ };
+
+ getCheckedIssues = () => {
+ const issues = this.state.checked.map(checked =>
+ this.state.issues.find(issue => issue.key === checked));
+ const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length };
+ return Promise.resolve({ issues, paging });
+ };
+
+ handleFilterChange = (changes: {}) => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery({ ...this.state.query, ...changes }),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ handleMyIssuesChange = (myIssues: boolean) => {
+ this.closeFacet('assignees');
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
+ id: this.props.component && this.props.component.key,
+ myIssues: myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ closeFacet = (property: string) => {
+ this.setState(state => ({
+ openFacets: { ...state.openFacets, [property]: false }
+ }));
+ };
+
+ handleFacetToggle = (property: string) => {
+ this.setState(state => ({
+ openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
+ }));
+ };
+
+ handleReset = () => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...DEFAULT_QUERY,
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ handleIssueCheck = (issue: string) => {
+ this.setState(state => ({
+ checked: state.checked.includes(issue)
+ ? without(state.checked, issue)
+ : [...state.checked, issue]
+ }));
+ };
+
+ handleIssueChange = (issue: Issue) => {
+ this.setState(state => ({
+ issues: state.issues.map(candidate => candidate.key === issue.key ? issue : candidate)
+ }));
+ };
+
+ openBulkChange = (mode: 'all' | 'selected') => {
+ this.setState({ bulkChange: mode });
+ key.setScope('issues-bulk-change');
+ };
+
+ closeBulkChange = () => {
+ key.setScope('issues');
+ this.setState({ bulkChange: null });
+ };
+
+ handleBulkChangeClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.openBulkChange('all');
+ };
+
+ handleBulkChangeSelectedClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.openBulkChange('selected');
+ };
+
+ handleBulkChangeDone = () => {
+ this.fetchFirstIssues();
+ this.closeBulkChange();
+ };
+
+ renderBulkChange(openIssue?: Issue) {
+ const { component, currentUser } = this.props;
+ const { bulkChange, checked, paging } = this.state;
+
+ if (!currentUser.isLoggedIn || openIssue != null) {
+ return null;
+ }
+
+ return (
+ <div className="pull-left">
+ {checked.length > 0
+ ? <div className="dropdown">
+ <button id="issues-bulk-change" data-toggle="dropdown">
+ {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>
+ : <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}>
+ {translate('bulk_change')}
+ </button>}
+ {bulkChange != null &&
+ <BulkChangeModal
+ component={component}
+ currentUser={currentUser}
+ fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues}
+ onClose={this.closeBulkChange}
+ onDone={this.handleBulkChangeDone}
+ onRequestFail={this.props.onRequestFail}
+ />}
+ </div>
+ );
+ }
+
+ renderList(openIssue?: Issue) {
+ const { component, currentUser } = this.props;
+ const { issues, paging } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;
+
+ if (paging == null) {
+ return null;
+ }
+
+ return (
+ <div className={openIssue != null ? 'hidden' : undefined}>
+ {paging.total > 0 &&
+ <IssuesList
+ checked={this.state.checked}
+ component={component}
+ issues={issues}
+ onFilterChange={this.handleFilterChange}
+ onIssueChange={this.handleIssueChange}
+ onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined}
+ onIssueClick={this.openIssue}
+ selectedIssue={selectedIssue}
+ />}
+
+ {paging.total > 0 &&
+ <ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
+
+ {paging.total === 0 && <EmptySearch />}
+ </div>
+ );
+ }
+
+ render() {
+ const { component, currentUser } = this.props;
+ const { issues, paging, query } = this.state;
+
+ const open = getOpen(this.props.location.query);
+ const openIssue = issues.find(issue => issue.key === open);
+
+ const selectedIndex = this.getSelectedIndex();
+
+ const top = component ? 95 : 30;
+
+ return (
+ <Page className="issues" id="issues-page">
+ <Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />
+
+ <PageSide top={top}>
+ <PageFilters>
+ {currentUser.isLoggedIn &&
+ <MyIssuesFilter
+ myIssues={this.state.myIssues}
+ onMyIssuesChange={this.handleMyIssuesChange}
+ />}
+ <FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
+ <Sidebar
+ component={component}
+ facets={this.state.facets}
+ myIssues={this.state.myIssues}
+ onFacetToggle={this.handleFacetToggle}
+ onFilterChange={this.handleFilterChange}
+ openFacets={this.state.openFacets}
+ query={query}
+ referencedComponents={this.state.referencedComponents}
+ referencedLanguages={this.state.referencedLanguages}
+ referencedRules={this.state.referencedRules}
+ referencedUsers={this.state.referencedUsers}
+ />
+ </PageFilters>
+ </PageSide>
+
+ <PageMain>
+ <HeaderPanel border={true} top={top}>
+ <PageMainInner>
+ {this.renderBulkChange(openIssue)}
+ {openIssue != null &&
+ <div className="pull-left">
+ <ComponentBreadcrumbs component={component} issue={openIssue} />
+ </div>}
+ <PageActions
+ loading={this.state.loading}
+ openIssue={openIssue}
+ paging={paging}
+ selectedIndex={selectedIndex}
+ />
+ </PageMainInner>
+ </HeaderPanel>
+
+ <PageMainInner>
+ <div>
+ {openIssue != null &&
+ <IssuesSourceViewer
+ openIssue={openIssue}
+ loadIssues={this.fetchIssuesForComponent}
+ onIssueChange={this.handleIssueChange}
+ onIssueSelect={this.openIssue}
+ />}
+
+ {this.renderList(openIssue)}
+ </div>
+ </PageMainInner>
+ </PageMain>
+ </Page>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
index 061b3e8f8c6..c605961dcad 100644
--- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
@@ -17,36 +17,38 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+// @flow
import { connect } from 'react-redux';
-import init from '../init';
+import { withRouter } from 'react-router';
+import type { Dispatch } from 'redux';
+import App from './App';
+import { onFail } from '../../../store/rootActions';
import { getComponent, getCurrentUser } from '../../../store/rootReducer';
+import { searchIssues } from '../../../api/issues';
+import { parseIssueFromResponse } from '../../../helpers/issues';
-class ComponentIssuesAppContainer extends React.Component {
- componentDidMount() {
- this.stop = init(this.refs.container, this.props.component, this.props.currentUser);
- }
-
- componentWillUnmount() {
- this.stop();
- }
-
- render() {
- // placing container inside div is required,
- // because when backbone.marionette's layout is destroyed,
- // it also destroys the root element,
- // but react wants it to be there to unmount it
- return (
- <div>
- <div ref="container" />
- </div>
- );
- }
-}
+type Query = { [string]: string };
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
+ component: ownProps.location.query.id
+ ? getComponent(state, ownProps.location.query.id)
+ : undefined,
currentUser: getCurrentUser(state)
});
-export default connect(mapStateToProps)(ComponentIssuesAppContainer);
+const fetchIssues = (query: Query) =>
+ (dispatch: Dispatch<*>) =>
+ searchIssues({ ...query, additionalFields: '_all' }).then(
+ response => {
+ const parsedIssues = response.issues.map(issue =>
+ parseIssueFromResponse(issue, response.components, response.users, response.rules));
+ return { ...response, issues: parsedIssues };
+ },
+ onFail(dispatch)
+ );
+
+const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error);
+
+const mapDispatchToProps = { fetchIssues, onRequestFail };
+
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
new file mode 100644
index 00000000000..3b7d454aa72
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
@@ -0,0 +1,522 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import Select from 'react-select';
+import { css } from 'glamor';
+import { pickBy, sortBy } from 'lodash';
+import SearchSelect from './SearchSelect';
+import Checkbox from '../../../components/controls/Checkbox';
+import Tooltip from '../../../components/controls/Tooltip';
+import MarkdownTips from '../../../components/common/MarkdownTips';
+import SeverityHelper from '../../../components/shared/SeverityHelper';
+import Avatar from '../../../components/ui/Avatar';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { searchAssignees } from '../utils';
+import type { Paging, Component, CurrentUser } from '../utils';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {|
+ component?: Component,
+ currentUser: CurrentUser,
+ fetchIssues: ({}) => Promise<*>,
+ onClose: () => void,
+ onDone: () => void,
+ onRequestFail: (Error) => void
+|};
+
+type State = {|
+ issues: Array<Issue>,
+ // used for initial loading of issues
+ loading: boolean,
+ paging?: Paging,
+ // used when submitting a form
+ submitting: boolean,
+ tags?: Array<string>,
+
+ // form fields
+ addTags?: Array<string>,
+ assignee?: string,
+ comment?: string,
+ notifications?: boolean,
+ removeTags?: Array<string>,
+ severity?: string,
+ transition?: string,
+ type?: string
+|};
+
+const hasAction = (action: string) =>
+ (issue: Issue): boolean => issue.actions && issue.actions.includes(action);
+
+export default class BulkChangeModal extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { issues: [], loading: true, submitting: false };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ Promise.all([this.loadIssues(), searchIssueTags()]).then(([issues, tags]) => {
+ if (this.mounted) {
+ this.setState({
+ issues: issues.issues,
+ loading: false,
+ paging: issues.paging,
+ tags
+ });
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCloseClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.props.onClose();
+ };
+
+ loadIssues = () => {
+ return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
+ };
+
+ handleAssigneeSearch = (query: string) => {
+ if (query.length > 1) {
+ return searchAssignees(query, this.props.component);
+ } else {
+ const { currentUser } = this.props;
+ const { issues } = this.state;
+ const options = [];
+
+ if (currentUser.isLoggedIn) {
+ const canBeAssignedToMe = issues.filter(
+ issue => issue.assignee !== currentUser.login
+ ).length > 0;
+ if (canBeAssignedToMe) {
+ options.push({
+ email: currentUser.email,
+ label: currentUser.name,
+ value: currentUser.login
+ });
+ }
+ }
+
+ const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0;
+ if (canBeUnassigned) {
+ options.push({ label: translate('unassigned'), value: '' });
+ }
+
+ return Promise.resolve(options);
+ }
+ };
+
+ handleAssigneeSelect = (assignee: string) => {
+ this.setState({ assignee });
+ };
+
+ handleFieldCheck = (field: string) =>
+ (checked: boolean) => {
+ if (!checked) {
+ this.setState({ [field]: undefined });
+ } else if (field === 'notifications') {
+ this.setState({ [field]: true });
+ }
+ };
+
+ handleFieldChange = (field: string) =>
+ (event: { target: HTMLInputElement }) => {
+ this.setState({ [field]: event.target.value });
+ };
+
+ handleSelectFieldChange = (field: string) =>
+ ({ value }: { value: string }) => {
+ this.setState({ [field]: value });
+ };
+
+ handleMultiSelectFieldChange = (field: string) =>
+ (options: Array<{ value: string }>) => {
+ this.setState({ [field]: options.map(option => option.value) });
+ };
+
+ handleSubmit = (e: Event) => {
+ e.preventDefault();
+ const query = pickBy({
+ assign: this.state.assignee,
+ set_type: this.state.type,
+ set_severity: this.state.severity,
+ add_tags: this.state.addTags && this.state.addTags.join(),
+ remove_tags: this.state.removeTags && this.state.removeTags.join(),
+ do_transition: this.state.transition,
+ comment: this.state.comment,
+ sendNotifications: this.state.notifications
+ });
+ const issueKeys = this.state.issues.map(issue => issue.key);
+
+ this.setState({ submitting: true });
+ bulkChangeIssues(issueKeys, query).then(
+ () => {
+ this.setState({ submitting: false });
+ this.props.onDone();
+ },
+ (error: Error) => {
+ this.setState({ submitting: false });
+ this.props.onRequestFail(error);
+ }
+ );
+ };
+
+ getAvailableTransitions(issues: Array<Issue>): Array<{ transition: string, count: number }> {
+ const transitions = {};
+ issues.forEach(issue => {
+ if (issue.transitions) {
+ issue.transitions.forEach(t => {
+ if (transitions[t] != null) {
+ transitions[t]++;
+ } else {
+ transitions[t] = 1;
+ }
+ });
+ }
+ });
+ return sortBy(Object.keys(transitions)).map(transition => ({
+ transition,
+ count: transitions[transition]
+ }));
+ }
+
+ renderCancelButton = () => (
+ <a id="bulk-change-cancel" href="#" onClick={this.handleCloseClick}>
+ {translate('cancel')}
+ </a>
+ );
+
+ renderLoading = () => (
+ <div>
+ <div className="modal-head">
+ <h2>{translate('bulk_change')}</h2>
+ </div>
+ <div className="modal-body">
+ <div className="text-center">
+ <i className="spinner spinner-margin" />
+ </div>
+ </div>
+ <div className="modal-foot">
+ {this.renderCancelButton()}
+ </div>
+ </div>
+ );
+
+ renderCheckbox = (field: string) => (
+ <Checkbox
+ className={css({ paddingTop: 6, paddingRight: 8 })}
+ checked={this.state[field] != null}
+ onCheck={this.handleFieldCheck(field)}
+ />
+ );
+
+ renderAffected = (affected: number) => (
+ <div className="pull-right note">
+ ({translateWithParameters('issue_bulk_change.x_issues', affected)})
+ </div>
+ );
+
+ renderField = (field: string, label: string, affected: ?number, input: Object) => (
+ <div className="modal-field" id={`issues-bulk-change-${field}`}>
+ <label htmlFor={field}>{translate(label)}</label>
+ {this.renderCheckbox(field)}
+ {input}
+ {affected != null && this.renderAffected(affected)}
+ </div>
+ );
+
+ renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => (
+ <span>
+ {(option.avatar != null || option.email != null) &&
+ <Avatar
+ className="little-spacer-right"
+ email={option.email}
+ hash={option.avatar}
+ size={16}
+ />}
+ {option.label}
+ </span>
+ );
+
+ renderAssigneeField = () => {
+ const affected: number = this.state.issues.filter(hasAction('assign')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const input = (
+ <SearchSelect
+ onSearch={this.handleAssigneeSearch}
+ onSelect={this.handleAssigneeSelect}
+ minimumQueryLength={0}
+ renderOption={this.renderAssigneeOption}
+ resetOnBlur={false}
+ value={this.state.assignee}
+ />
+ );
+
+ return this.renderField('assignee', 'issue.assign.formlink', affected, input);
+ };
+
+ renderTypeField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_type')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+ const options = types.map(type => ({ label: translate('issue.type', type), value: type }));
+
+ const optionRenderer = (option: { label: string, value: string }) => (
+ <span>
+ <IssueTypeIcon className="little-spacer-right" query={option.value} />
+ {option.label}
+ </span>
+ );
+
+ const input = (
+ <Select
+ clearable={false}
+ id="type"
+ onChange={this.handleSelectFieldChange('type')}
+ options={options}
+ optionRenderer={optionRenderer}
+ searchable={false}
+ value={this.state.type}
+ valueRenderer={optionRenderer}
+ />
+ );
+
+ return this.renderField('type', 'issue.set_type', affected, input);
+ };
+
+ renderSeverityField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_severity')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
+ const options = severities.map(severity => ({
+ label: translate('severity', severity),
+ value: severity
+ }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="severity"
+ onChange={this.handleSelectFieldChange('severity')}
+ options={options}
+ optionRenderer={option => <SeverityHelper severity={option.value} />}
+ searchable={false}
+ value={this.state.severity}
+ valueRenderer={option => <SeverityHelper severity={option.value} />}
+ />
+ );
+
+ return this.renderField('severity', 'issue.set_severity', affected, input);
+ };
+
+ renderAddTagsField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_tags')).length;
+
+ if (this.state.tags == null || affected === 0) {
+ return null;
+ }
+
+ const options = this.state.tags.map(tag => ({ label: tag, value: tag }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="add_tags"
+ multi={true}
+ onChange={this.handleMultiSelectFieldChange('addTags')}
+ options={options}
+ searchable={true}
+ value={this.state.addTags}
+ />
+ );
+
+ return this.renderField('addTags', 'issue.add_tags', affected, input);
+ };
+
+ renderRemoveTagsField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_tags')).length;
+
+ if (this.state.tags == null || affected === 0) {
+ return null;
+ }
+
+ const options = this.state.tags.map(tag => ({ label: tag, value: tag }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="remove_tags"
+ multi={true}
+ onChange={this.handleMultiSelectFieldChange('removeTags')}
+ options={options}
+ searchable={true}
+ value={this.state.removeTags}
+ />
+ );
+
+ return this.renderField('removeTags', 'issue.remove_tags', affected, input);
+ };
+
+ renderTransitionsField = () => {
+ const transitions = this.getAvailableTransitions(this.state.issues);
+
+ if (transitions.length === 0) {
+ return null;
+ }
+
+ return (
+ <div className="modal-field">
+ <label>{translate('issue.transition')}</label>
+ {transitions.map(transition => (
+ <span key={transition.transition}>
+ <input
+ checked={this.state.transition === transition.transition}
+ id={`transition-${transition.transition}`}
+ name="do_transition.transition"
+ onChange={this.handleFieldChange('transition')}
+ type="radio"
+ value={transition.transition}
+ />
+ <label
+ htmlFor={`transition-${transition.transition}`}
+ style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}>
+ {translate('issue.transition', transition.transition)}
+ </label>
+ {this.renderAffected(transition.count)}
+ <br />
+ </span>
+ ))}
+ </div>
+ );
+ };
+
+ renderCommentField = () => {
+ const affected: number = this.state.issues.filter(hasAction('comment')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ return (
+ <div className="modal-field">
+ <label htmlFor="comment">
+ {translate('issue.comment.formlink')}
+ <Tooltip overlay={translate('issue_bulk_change.comment.help')}>
+ <i className="icon-help little-spacer-left" />
+ </Tooltip>
+ </label>
+ <div>
+ <textarea
+ id="comment"
+ onChange={this.handleFieldChange('comment')}
+ rows="4"
+ style={{ width: '100%' }}
+ value={this.state.comment || ''}
+ />
+ </div>
+ <div className="pull-right">
+ <MarkdownTips />
+ </div>
+ </div>
+ );
+ };
+
+ renderNotificationsField = () => (
+ <div className="modal-field">
+ <label htmlFor="send-notifications">{translate('issue.send_notifications')}</label>
+ {this.renderCheckbox('notifications')}
+ </div>
+ );
+
+ renderForm = () => {
+ const { issues, paging, submitting } = this.state;
+
+ const limitReached: boolean = paging != null &&
+ paging.total > paging.pageIndex * paging.pageSize;
+
+ return (
+ <form id="bulk-change-form" onSubmit={this.handleSubmit}>
+ <div className="modal-head">
+ <h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2>
+ </div>
+
+ <div className="modal-body">
+ {limitReached &&
+ <div className="alert alert-warning">
+ {translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)}
+ </div>}
+
+ {this.renderAssigneeField()}
+ {this.renderTypeField()}
+ {this.renderSeverityField()}
+ {this.renderAddTagsField()}
+ {this.renderRemoveTagsField()}
+ {this.renderTransitionsField()}
+ {this.renderCommentField()}
+ {this.renderNotificationsField()}
+ </div>
+
+ <div className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <button disabled={submitting} id="bulk-change-submit">{translate('apply')}</button>
+ {this.renderCancelButton()}
+ </div>
+ </form>
+ );
+ };
+
+ render() {
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel="modal"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ {this.state.loading ? this.renderLoading() : this.renderForm()}
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
new file mode 100644
index 00000000000..bc12da43bf1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import Organization from '../../../components/shared/Organization';
+import { collapsePath, limitComponentName } from '../../../helpers/path';
+import { getProjectUrl } from '../../../helpers/urls';
+import type { Component } from '../utils';
+
+type Props = {
+ component?: Component,
+ issue: Object
+};
+
+export default class ComponentBreadcrumbs extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { component, issue } = this.props;
+
+ const displayOrganization = component == null || ['VW', 'SVW'].includes(component.qualifier);
+ const displayProject = component == null ||
+ !['TRK', 'BRC', 'DIR'].includes(component.qualifier);
+ const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier);
+
+ return (
+ <div className="component-name">
+ {displayOrganization &&
+ <Organization linkClassName="link-no-underline" organizationKey={issue.organization} />}
+
+ {displayProject &&
+ <span>
+ <Link to={getProjectUrl(issue.project)} className="link-no-underline">
+ {limitComponentName(issue.projectName)}
+ </Link>
+ <span className="slash-separator" />
+ </span>}
+
+ {displaySubProject &&
+ issue.subProject != null &&
+ <span>
+ <Link to={getProjectUrl(issue.subProject)} className="link-no-underline">
+ {limitComponentName(issue.subProjectName)}
+ </Link>
+ <span className="slash-separator" />
+ </span>}
+
+ <Link to={getProjectUrl(issue.component)} className="link-no-underline">
+ {collapsePath(issue.componentLongName)}
+ </Link>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
index b1f3e70f3f0..9740f63e718 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
@@ -17,39 +17,39 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+// @flow
import React from 'react';
-import { connect } from 'react-redux';
-import init from '../init';
-import { getCurrentUser } from '../../../store/rootReducer';
+import { css } from 'glamor';
+import { translate } from '../../../helpers/l10n';
-class IssuesAppContainer extends React.Component {
- static propTypes = {
- currentUser: React.PropTypes.any.isRequired
- };
+type Props = {
+ displayReset: boolean,
+ onReset: () => void
+};
- componentDidMount() {
- this.stop = init(this.refs.container, this.props.currentUser);
- }
+const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });
- componentWillUnmount() {
- this.stop();
- }
+export default class FiltersHeader extends React.PureComponent {
+ props: Props;
+
+ handleResetClick = (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ this.props.onReset();
+ };
render() {
- // placing container inside div is required,
- // because when backbone.marionette's layout is destroyed,
- // it also destroys the root element,
- // but react wants it to be there to unmount it
return (
- <div>
- <div ref="container" />
+ <div className={styles}>
+ {this.props.displayReset &&
+ <div className={css({ float: 'right' })}>
+ <button className="button-red" onClick={this.handleResetClick}>
+ {translate('clear_all_filters')}
+ </button>
+ </div>}
+
+ <h3>{translate('filters')}</h3>
</div>
);
}
}
-
-const mapStateToProps = state => ({
- currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(IssuesAppContainer);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
new file mode 100644
index 00000000000..2d8530dab51
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css, media } from 'glamor';
+import { clearfix } from 'glamor/utils';
+import { throttle } from 'lodash';
+
+type Props = {|
+ border: boolean,
+ children?: React.Element<*>,
+ top?: number
+|};
+
+type State = {
+ scrolled: boolean
+};
+
+export default class HeaderPanel extends React.PureComponent {
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { scrolled: this.isScrolled() };
+ this.handleScroll = throttle(this.handleScroll, 50);
+ }
+
+ componentDidMount() {
+ if (this.props.top != null) {
+ window.addEventListener('scroll', this.handleScroll);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.top != null) {
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+ }
+
+ isScrolled = () => window.scrollY > 10;
+
+ handleScroll = () => {
+ this.setState({ scrolled: this.isScrolled() });
+ };
+
+ render() {
+ const commonStyles = {
+ height: 56,
+ lineHeight: '24px',
+ padding: '16px 20px',
+ boxSizing: 'border-box',
+ borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined,
+ backgroundColor: '#f3f3f3'
+ };
+
+ const inner = this.props.top
+ ? <div
+ className={css(
+ commonStyles,
+ {
+ position: 'fixed',
+ zIndex: 30,
+ top: this.props.top,
+ left: 'calc(50vw - 360px + 1px)',
+ right: 0,
+ boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none',
+ transition: 'box-shadow 0.3s ease'
+ },
+ media('(max-width: 1320px)', { left: 301 })
+ )}>
+ {this.props.children}
+ </div>
+ : this.props.children;
+
+ return (
+ <div
+ className={css(clearfix(), commonStyles, {
+ marginTop: -20,
+ marginBottom: 20,
+ marginLeft: -20,
+ marginRight: -20,
+ '& .component-name': { lineHeight: '24px' }
+ })}>
+ {inner}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
new file mode 100644
index 00000000000..4bd9cf945d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import ListItem from './ListItem';
+import type { Issue } from '../../../components/issue/types';
+import type { Component } from '../utils';
+
+type Props = {|
+ checked: Array<string>,
+ component?: Component,
+ issues: Array<Issue>,
+ onFilterChange: (changes: {}) => void,
+ onIssueChange: (Issue) => void,
+ onIssueCheck?: (string) => void,
+ onIssueClick: (string) => void,
+ selectedIssue: ?Issue
+|};
+
+export default class IssuesList extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { checked, component, issues, selectedIssue } = this.props;
+
+ return (
+ <div>
+ {issues.map((issue, index) => (
+ <ListItem
+ checked={checked.includes(issue.key)}
+ component={component}
+ key={issue.key}
+ issue={issue}
+ onChange={this.props.onIssueChange}
+ onCheck={this.props.onIssueCheck}
+ onClick={this.props.onIssueClick}
+ onFilterChange={this.props.onFilterChange}
+ previousIssue={index > 0 ? issues[index - 1] : null}
+ selected={selectedIssue != null && selectedIssue.key === issue.key}
+ />
+ ))}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
new file mode 100644
index 00000000000..ff090ed8287
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import { scrollToElement } from '../../../helpers/scrolling';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {|
+ loadIssues: () => Promise<*>,
+ onIssueChange: (Issue) => void,
+ onIssueSelect: (string) => void,
+ openIssue: Issue
+|};
+
+export default class IssuesSourceViewer extends React.PureComponent {
+ node: HTMLElement;
+ props: Props;
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.openIssue.component === this.props.openIssue.component) {
+ this.scrollToIssue();
+ }
+ }
+
+ scrollToIssue = () => {
+ const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
+ if (element) {
+ scrollToElement(element, 100, 100);
+ }
+ };
+
+ render() {
+ const { openIssue } = this.props;
+
+ return (
+ <div ref={node => this.node = node}>
+ <SourceViewer
+ aroundLine={openIssue.line}
+ component={openIssue.component}
+ displayAllIssues={true}
+ loadIssues={this.props.loadIssues}
+ onLoaded={this.scrollToIssue}
+ onIssueChange={this.props.onIssueChange}
+ onIssueSelect={this.props.onIssueSelect}
+ selectedIssue={openIssue.key}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
new file mode 100644
index 00000000000..f8a0bdda14a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import ComponentBreadcrumbs from './ComponentBreadcrumbs';
+import Issue from '../../../components/issue/Issue';
+import type { Issue as IssueType } from '../../../components/issue/types';
+import type { Component } from '../utils';
+
+type Props = {|
+ checked: boolean,
+ component?: Component,
+ issue: IssueType,
+ onChange: (IssueType) => void,
+ onCheck?: (string) => void,
+ onClick: (string) => void,
+ onFilterChange: (changes: {}) => void,
+ previousIssue: ?Object,
+ selected: boolean
+|};
+
+type State = {
+ similarIssues: boolean
+};
+
+export default class ListItem extends React.PureComponent {
+ props: Props;
+ state: State = { similarIssues: false };
+
+ handleFilter = (property: string, issue: IssueType) => {
+ const { onFilterChange } = this.props;
+
+ const issuesReset = { issues: [] };
+
+ if (property.startsWith('tag###')) {
+ const tag = property.substr(6);
+ return onFilterChange({ ...issuesReset, tags: [tag] });
+ }
+
+ switch (property) {
+ case 'type':
+ return onFilterChange({ ...issuesReset, types: [issue.type] });
+ case 'severity':
+ return onFilterChange({ ...issuesReset, severities: [issue.severity] });
+ case 'status':
+ return onFilterChange({ ...issuesReset, statuses: [issue.status] });
+ case 'resolution':
+ return issue.resolution != null
+ ? onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] })
+ : onFilterChange({ ...issuesReset, resolved: false, resolutions: [] });
+ case 'assignee':
+ return issue.assignee != null
+ ? onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] })
+ : onFilterChange({ ...issuesReset, assigned: false, assignees: [] });
+ case 'rule':
+ return onFilterChange({ ...issuesReset, rules: [issue.rule] });
+ case 'project':
+ return onFilterChange({ ...issuesReset, projects: [issue.projectUuid] });
+ case 'module':
+ return onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] });
+ case 'file':
+ return onFilterChange({ ...issuesReset, files: [issue.componentUuid] });
+ }
+ };
+
+ render() {
+ const { component, issue, previousIssue } = this.props;
+
+ const displayComponent = previousIssue == null || previousIssue.component !== issue.component;
+
+ return (
+ <div className="issues-workspace-list-item">
+ {displayComponent &&
+ <div className="issues-workspace-list-component">
+ <ComponentBreadcrumbs component={component} issue={this.props.issue} />
+ </div>}
+ <Issue
+ checked={this.props.checked}
+ issue={issue}
+ onChange={this.props.onChange}
+ onCheck={this.props.onCheck}
+ onClick={this.props.onClick}
+ onFilter={this.handleFilter}
+ selected={this.props.selected}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
new file mode 100644
index 00000000000..a8f6441b1fd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ myIssues: boolean,
+ onMyIssuesChange: (boolean) => void
+|};
+
+export default class MyIssuesFilter extends React.Component {
+ props: Props;
+
+ handleClick = (myIssues: boolean) =>
+ (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ this.props.onMyIssuesChange(myIssues);
+ };
+
+ render() {
+ const { myIssues } = this.props;
+
+ return (
+ <div className={css({ marginBottom: 24, textAlign: 'center' })}>
+ <div className="button-group">
+ <button
+ className={myIssues ? 'button-active' : undefined}
+ onClick={this.handleClick(true)}>
+ {translate('issues.my_issues')}
+ </button>
+ <button
+ className={myIssues ? undefined : 'button-active'}
+ onClick={this.handleClick(false)}>
+ {translate('all')}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
new file mode 100644
index 00000000000..262c3ad8699
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+import type { Paging } from '../utils';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+type Props = {|
+ loading: boolean,
+ openIssue: ?{},
+ paging: ?Paging,
+ selectedIndex: ?number
+|};
+
+export default class PageActions extends React.Component {
+ props: Props;
+
+ renderShortcuts() {
+ return (
+ <span className="note big-spacer-right">
+ <span className="big-spacer-right">
+ <span className="shortcut-button little-spacer-right">↑</span>
+ <span className="shortcut-button little-spacer-right">↓</span>
+ {translate('issues.to_select_issues')}
+ </span>
+
+ <span>
+ <span className="shortcut-button little-spacer-right">←</span>
+ <span className="shortcut-button little-spacer-right">→</span>
+ {translate('issues.to_navigate')}
+ </span>
+ </span>
+ );
+ }
+
+ render() {
+ const { openIssue, paging, selectedIndex } = this.props;
+
+ return (
+ <div className={css({ float: 'right' })}>
+ {openIssue == null && this.renderShortcuts()}
+
+ <div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
+ {this.props.loading && <i className="spinner spacer-right" />}
+ {paging != null &&
+ <span>
+ <strong>
+ {selectedIndex != null && <span>{selectedIndex + 1} / </span>}
+ {formatMeasure(paging.total, 'INT')}
+ </strong>
+ {' '}
+ {translate('issues.issues')}
+ </span>}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js
new file mode 100644
index 00000000000..eddea6983ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { debounce } from 'lodash';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ minimumQueryLength: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>,
+ resetOnBlur: boolean,
+ value?: string
+|};
+
+type State = {
+ loading: boolean,
+ options: Array<Option>,
+ query: string
+};
+
+export default class SearchSelect extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ static defaultProps = {
+ minimumQueryLength: 2,
+ resetOnBlur: true
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { loading: false, options: [], query: '' };
+ this.search = debounce(this.search, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ search = (query: string) => {
+ this.props.onSearch(query).then(options => {
+ if (this.mounted) {
+ this.setState({ loading: false, options });
+ }
+ });
+ };
+
+ handleBlur = () => {
+ this.setState({ options: [], query: '' });
+ };
+
+ handleChange = (option: Option) => {
+ this.props.onSelect(option.value);
+ };
+
+ handleInputChange = (query: string = '') => {
+ if (query.length >= this.props.minimumQueryLength) {
+ this.setState({ loading: true, query });
+ this.search(query);
+ } else {
+ this.setState({ options: [], query });
+ }
+ };
+
+ // disable internal filtering
+ handleFilterOption = () => true;
+
+ render() {
+ return (
+ <Select
+ autofocus={true}
+ cache={false}
+ className="input-super-large"
+ clearable={false}
+ filterOption={this.handleFilterOption}
+ isLoading={this.state.loading}
+ noResultsText={
+ this.state.query.length < this.props.minimumQueryLength
+ ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength)
+ : translate('select2.noMatches')
+ }
+ onBlur={this.props.resetOnBlur ? this.handleBlur : undefined}
+ onChange={this.handleChange}
+ onInputChange={this.handleInputChange}
+ onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined}
+ optionRenderer={this.props.renderOption}
+ options={this.state.options}
+ placeholder={translate('search_verb')}
+ searchable={true}
+ value={this.props.value}
+ valueRenderer={this.props.renderOption}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js
new file mode 100644
index 00000000000..f4d46d4f8a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import SearchSelect from '../SearchSelect';
+
+jest.mock('lodash', () => ({
+ debounce: fn => fn
+}));
+
+it('should render Select', () => {
+ expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('should call onSelect', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
+ wrapper.prop('onChange')({ value: 'foo' });
+ expect(onSelect).lastCalledWith('foo');
+});
+
+it('should call onSearch', () => {
+ const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
+ const wrapper = shallow(
+ <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
+ );
+ wrapper.prop('onInputChange')('f');
+ expect(onSearch).not.toHaveBeenCalled();
+ wrapper.prop('onInputChange')('foo');
+ expect(onSearch).lastCalledWith('foo');
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap
new file mode 100644
index 00000000000..4f2fe37cc62
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap
@@ -0,0 +1,48 @@
+exports[`test should render Select 1`] = `
+<Select
+ addLabelText="Add \"{label}\"?"
+ arrowRenderer={[Function]}
+ autofocus={true}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ cache={false}
+ className="input-super-large"
+ clearAllText="Clear all"
+ clearValueText="Clear value"
+ clearable={false}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ filterOption={[Function]}
+ filterOptions={[Function]}
+ ignoreAccents={true}
+ ignoreCase={true}
+ inputProps={Object {}}
+ isLoading={false}
+ joinValues={false}
+ labelKey="label"
+ matchPos="any"
+ matchProp="any"
+ menuBuffer={0}
+ menuRenderer={[Function]}
+ multi={false}
+ noResultsText="select2.tooShort.2"
+ onBlur={[Function]}
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ onInputChange={[Function]}
+ openAfterFocus={false}
+ optionComponent={[Function]}
+ options={Array []}
+ pageSize={5}
+ placeholder="search_verb"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ valueComponent={[Function]}
+ valueKey="value" />
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js
deleted file mode 100644
index f98f292d4bd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/controller.js
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import Backbone from 'backbone';
-import Controller from '../../components/navigator/controller';
-import ComponentViewer from './component-viewer/main';
-import getStore from '../../app/utils/getStore';
-import { receiveIssues } from '../../store/issues/duck';
-
-const FACET_DATA_FIELDS = ['components', 'users', 'rules', 'languages'];
-
-export default Controller.extend({
- _issuesParameters() {
- return {
- p: this.options.app.state.get('page'),
- ps: this.pageSize,
- asc: true,
- additionalFields: '_all',
- facets: this._facetsFromServer().join()
- };
- },
-
- receiveIssues(issues) {
- const store = getStore();
- store.dispatch(receiveIssues(issues));
- },
-
- fetchList(firstPage) {
- const that = this;
- if (firstPage == null) {
- firstPage = true;
- }
- if (firstPage) {
- this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true });
- this.closeComponentViewer();
- }
- const data = this._issuesParameters();
- Object.assign(data, this.options.app.state.get('query'));
- if (this.options.app.state.get('query').assigned_to_me) {
- Object.assign(data, { assignees: '__me__' });
- }
- if (this.options.app.state.get('isContext')) {
- Object.assign(data, this.options.app.state.get('contextQuery'));
- }
- return $.get(window.baseUrl + '/api/issues/search', data).done(r => {
- const issues = that.options.app.list.parseIssues(r);
- this.receiveIssues(issues);
- if (firstPage) {
- const issues = that.options.app.list.parseIssues(r);
- that.options.app.list.reset(issues);
- } else {
- const issues = that.options.app.list.parseIssues(r, that.options.app.list.length);
- that.options.app.list.add(issues);
- }
- that.options.app.list.setIndex();
- FACET_DATA_FIELDS.forEach(field => {
- that.options.app.facets[field] = r[field];
- });
- that.options.app.facets.reset(that._allFacets());
- that.options.app.facets.add(r.facets, { merge: true });
- that.enableFacets(that._enabledFacets());
- if (firstPage) {
- that.options.app.state.set({
- page: r.p,
- pageSize: r.ps,
- total: r.total,
- maxResultsReached: r.p * r.ps >= r.total
- });
- } else {
- that.options.app.state.set({
- page: r.p,
- maxResultsReached: r.p * r.ps >= r.total
- });
- }
- if (firstPage && that.isIssuePermalink()) {
- that.showComponentViewer(that.options.app.list.first());
- }
- });
- },
-
- isIssuePermalink() {
- const query = this.options.app.state.get('query');
- return query.issues != null && this.options.app.list.length === 1;
- },
-
- _mergeCollections(a, b) {
- const collection = new Backbone.Collection(a);
- collection.add(b, { merge: true });
- return collection.toJSON();
- },
-
- requestFacet(id) {
- const that = this;
- const facet = this.options.app.facets.get(id);
- const data = {
- facets: id,
- ps: 1,
- additionalFields: '_all',
- ...this.options.app.state.get('query')
- };
- if (this.options.app.state.get('query').assigned_to_me) {
- Object.assign(data, { assignees: '__me__' });
- }
- if (this.options.app.state.get('isContext')) {
- Object.assign(data, this.options.app.state.get('contextQuery'));
- }
- return $.get(window.baseUrl + '/api/issues/search', data, r => {
- FACET_DATA_FIELDS.forEach(field => {
- that.options.app.facets[field] = that._mergeCollections(
- that.options.app.facets[field],
- r[field]
- );
- });
- const facetData = r.facets.find(facet => facet.property === id);
- if (facetData != null) {
- facet.set(facetData);
- }
- });
- },
-
- newSearch() {
- this.options.app.state.unset('filter');
- return this.options.app.state.setQuery({ resolved: 'false' });
- },
-
- parseQuery() {
- const q = Controller.prototype.parseQuery.apply(this, arguments);
- delete q.asc;
- delete q.s;
- delete q.id;
- return q;
- },
-
- getQueryAsObject() {
- const state = this.options.app.state;
- const query = state.get('query');
- if (query.assigned_to_me) {
- Object.assign(query, { assignees: '__me__' });
- }
- if (state.get('isContext')) {
- Object.assign(query, state.get('contextQuery'));
- }
- return query;
- },
-
- getQuery(separator, addContext, handleMyIssues = false) {
- if (separator == null) {
- separator = '|';
- }
- if (addContext == null) {
- addContext = false;
- }
- const filter = this.options.app.state.get('query');
- if (addContext && this.options.app.state.get('isContext')) {
- Object.assign(filter, this.options.app.state.get('contextQuery'));
- }
- if (handleMyIssues && this.options.app.state.get('query').assigned_to_me) {
- Object.assign(filter, { assignees: '__me__' });
- }
- const route = [];
- Object.keys(filter).forEach(property => {
- route.push(`${property}=${encodeURIComponent(filter[property])}`);
- });
- return route.join(separator);
- },
-
- _prepareComponent(issue) {
- return {
- key: issue.get('component'),
- name: issue.get('componentLongName'),
- qualifier: issue.get('componentQualifier'),
- subProject: issue.get('subProject'),
- subProjectName: issue.get('subProjectLongName'),
- project: issue.get('project'),
- projectName: issue.get('projectLongName'),
- projectOrganization: issue.get('projectOrganization')
- };
- },
-
- showComponentViewer(issue) {
- this.options.app.layout.workspaceComponentViewerRegion.reset();
- key.setScope('componentViewer');
- this.options.app.issuesView.unbindScrollEvents();
- this.options.app.state.set('component', this._prepareComponent(issue));
- this.options.app.componentViewer = new ComponentViewer({ app: this.options.app });
- this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer);
- this.options.app.layout.showComponentViewer();
- return this.options.app.componentViewer.openFileByIssue(issue);
- },
-
- closeComponentViewer() {
- key.setScope('list');
- $('body').click();
- this.options.app.state.unset('component');
- this.options.app.layout.workspaceComponentViewerRegion.reset();
- this.options.app.layout.hideComponentViewer();
- this.options.app.issuesView.bindScrollEvents();
- return this.options.app.issuesView.scrollTo();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets-view.js b/server/sonar-web/src/main/js/apps/issues/facets-view.js
deleted file mode 100644
index f706ef53a39..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets-view.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import FacetsView from '../../components/navigator/facets-view';
-import BaseFacet from './facets/base-facet';
-import TypeFacet from './facets/type-facet';
-import SeverityFacet from './facets/severity-facet';
-import StatusFacet from './facets/status-facet';
-import ProjectFacet from './facets/project-facet';
-import ModuleFacet from './facets/module-facet';
-import AssigneeFacet from './facets/assignee-facet';
-import RuleFacet from './facets/rule-facet';
-import TagFacet from './facets/tag-facet';
-import ResolutionFacet from './facets/resolution-facet';
-import CreationDateFacet from './facets/creation-date-facet';
-import FileFacet from './facets/file-facet';
-import LanguageFacet from './facets/language-facet';
-import AuthorFacet from './facets/author-facet';
-import IssueKeyFacet from './facets/issue-key-facet';
-import ContextFacet from './facets/context-facet';
-import ModeFacet from './facets/mode-facet';
-
-const viewsMapping = {
- types: TypeFacet,
- severities: SeverityFacet,
- statuses: StatusFacet,
- assignees: AssigneeFacet,
- resolutions: ResolutionFacet,
- createdAt: CreationDateFacet,
- projectUuids: ProjectFacet,
- moduleUuids: ModuleFacet,
- rules: RuleFacet,
- tags: TagFacet,
- fileUuids: FileFacet,
- languages: LanguageFacet,
- authors: AuthorFacet,
- issues: IssueKeyFacet,
- context: ContextFacet,
- facetMode: ModeFacet
-};
-
-export default FacetsView.extend({
- getChildView(model) {
- const view = viewsMapping[model.get('property')];
- return view ? view : BaseFacet;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
deleted file mode 100644
index 597afcaee47..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import { sortBy } from 'lodash';
-import CustomValuesFacet from './custom-values-facet';
-import Template from '../templates/facets/issues-assignee-facet.hbs';
-
-export default CustomValuesFacet.extend({
- template: Template,
-
- initialize() {
- this.context = {
- isContext: this.options.app.state.get('isContext'),
- organization: this.options.app.state.get('contextOrganization')
- };
- },
-
- getUrl() {
- return window.baseUrl +
- (this.context.isContext ? '/api/organizations/search_members' : '/api/users/search');
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data: (term, page) => {
- if (this.context.isContext && this.context.organization) {
- return { q: term, p: page, organization: this.context.organization };
- } else {
- return { q: term, p: page };
- }
- },
- results: window.usersToSelect2
- };
- },
-
- onRender() {
- CustomValuesFacet.prototype.onRender.apply(this, arguments);
-
- const myIssuesSelected = !!this.options.app.state.get('query').assigned_to_me;
- this.$el.toggleClass('hidden', myIssuesSelected);
-
- const value = this.options.app.state.get('query').assigned;
- if (value != null && (!value || value === 'false')) {
- this.$('.js-facet').filter('[data-unassigned]').addClass('active');
- }
- },
-
- toggleFacet(e) {
- const unassigned = $(e.currentTarget).is('[data-unassigned]');
- $(e.currentTarget).toggleClass('active');
- if (unassigned) {
- const checked = $(e.currentTarget).is('.active');
- const value = checked ? 'false' : null;
- return this.options.app.state.updateFilter({
- assigned: value,
- assignees: null,
- assigned_to_me: null
- });
- } else {
- return this.options.app.state.updateFilter({
- assigned: null,
- assignees: this.getValue(),
- assigned_to_me: null
- });
- }
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const users = this.options.app.facets.users;
- values.forEach(v => {
- const login = v.val;
- let name = '';
- if (login) {
- const user = users.find(user => user.login === login);
- if (user != null) {
- name = user.name;
- }
- }
- v.label = name;
- });
- return values;
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- assigned: null,
- assignees: null
- });
- },
-
- addCustomValue() {
- const property = this.model.get('property');
- const customValue = this.$('.js-custom-value').select2('val');
- let value = this.getValue();
- if (value.length > 0) {
- value += ',';
- }
- value += customValue;
- const obj = {};
- obj[property] = value;
- obj.assigned = null;
- return this.options.app.state.updateFilter(obj);
- },
-
- sortValues(values) {
- return sortBy(values, v => v.val === '' ? -999999 : -v.count);
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
deleted file mode 100644
index c652eba0abd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/issues/authors';
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- quietMillis: 300,
- url: this.getUrl(),
- data(term) {
- return { q: term, ps: 25 };
- },
- results(data) {
- return {
- more: false,
- results: data.authors.map(author => {
- return { id: author, text: author };
- })
- };
- }
- }
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
deleted file mode 100644
index 5d05caaeb2e..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import BaseFacet from '../../../components/navigator/facets/base-facet';
-import Template from '../templates/facets/issues-base-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- },
-
- onDestroy() {
- return this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- state: this.options.app.state.toJSON()
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
deleted file mode 100644
index a2f31cb9c37..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-context-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- state: this.options.app.state.toJSON()
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
deleted file mode 100644
index 81c31973b79..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import moment from 'moment';
-import { times } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-creation-date-facet.hbs';
-import '../../../components/widgets/barchart';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default BaseFacet.extend({
- template: Template,
-
- events() {
- return {
- ...BaseFacet.prototype.events.apply(this, arguments),
- 'change input': 'applyFacet',
- 'click .js-select-period-start': 'selectPeriodStart',
- 'click .js-select-period-end': 'selectPeriodEnd',
- 'click .sonar-d3 rect': 'selectBar',
- 'click .js-all': 'onAllClick',
- 'click .js-last-week': 'onLastWeekClick',
- 'click .js-last-month': 'onLastMonthClick',
- 'click .js-last-year': 'onLastYearClick',
- 'click .js-leak': 'onLeakClick'
- };
- },
-
- onRender() {
- const that = this;
- this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled'));
- this.$('input').datepicker({
- dateFormat: 'yy-mm-dd',
- changeMonth: true,
- changeYear: true
- });
- const props = ['createdAfter', 'createdBefore', 'createdAt'];
- const query = this.options.app.state.get('query');
- props.forEach(prop => {
- const value = query[prop];
- if (value != null) {
- that.$(`input[name=${prop}]`).val(value);
- }
- });
- let values = this.model.getValues();
- if (!(Array.isArray(values) && values.length > 0)) {
- let date = moment();
- values = [];
- times(10, () => {
- values.push({ count: 0, val: date.toDate().toString() });
- date = date.subtract(1, 'days');
- });
- values.reverse();
- }
- values = values.map(v => {
- const format = that.options.app.state.getFacetMode() === 'count'
- ? 'SHORT_INT'
- : 'SHORT_WORK_DUR';
- const text = formatMeasure(v.count, format);
- return { ...v, text };
- });
- return this.$('.js-barchart').barchart(values);
- },
-
- selectPeriodStart() {
- return this.$('.js-period-start').datepicker('show');
- },
-
- selectPeriodEnd() {
- return this.$('.js-period-end').datepicker('show');
- },
-
- applyFacet() {
- const obj = { createdAt: null, createdInLast: null };
- this.$('input').each(function() {
- const property = $(this).prop('name');
- const value = $(this).val();
- obj[property] = value;
- });
- return this.options.app.state.updateFilter(obj);
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: null
- });
- },
-
- selectBar(e) {
- const periodStart = $(e.currentTarget).data('period-start');
- const periodEnd = $(e.currentTarget).data('period-end');
- return this.options.app.state.updateFilter({
- createdAfter: periodStart,
- createdBefore: periodEnd,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: null
- });
- },
-
- selectPeriod(period) {
- return this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: period
- });
- },
-
- onAllClick(e) {
- e.preventDefault();
- return this.disable();
- },
-
- onLastWeekClick(e) {
- e.preventDefault();
- return this.selectPeriod('1w');
- },
-
- onLastMonthClick(e) {
- e.preventDefault();
- return this.selectPeriod('1m');
- },
-
- onLastYearClick(e) {
- e.preventDefault();
- return this.selectPeriod('1y');
- },
-
- onLeakClick(e) {
- e.preventDefault();
- this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- createdInLast: null,
- sinceLeakPeriod: 'true'
- });
- },
-
- serializeData() {
- const hasLeak = this.options.app.state.get('contextComponentQualifier') === 'TRK';
-
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- hasLeak,
- periodStart: this.options.app.state.get('query').createdAfter,
- periodEnd: this.options.app.state.get('query').createdBefore,
- createdAt: this.options.app.state.get('query').createdAt,
- sinceLeakPeriod: this.options.app.state.get('query').sinceLeakPeriod,
- createdInLast: this.options.app.state.get('query').createdInLast
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
deleted file mode 100644
index a6168f350f1..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-custom-values-facet.hbs';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default BaseFacet.extend({
- template: Template,
-
- events() {
- return {
- ...BaseFacet.prototype.events.apply(this, arguments),
- 'change .js-custom-value': 'addCustomValue'
- };
- },
-
- getUrl() {},
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- return this.prepareSearch();
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: this.prepareAjaxSearch()
- });
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { s: term, p: page };
- },
- results(data) {
- return { more: data.more, results: data.results };
- }
- };
- },
-
- addCustomValue() {
- const property = this.model.get('property');
- const customValue = this.$('.js-custom-value').select2('val');
- let value = this.getValue();
- if (value.length > 0) {
- value += ',';
- }
- value += customValue;
- const obj = {};
- obj[property] = value;
- return this.options.app.state.updateFilter(obj);
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
deleted file mode 100644
index 11b9a69a46d..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-file-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- const widths = this.$('.facet-stat')
- .map(function() {
- return $(this).outerWidth();
- })
- .get();
- const maxValueWidth = Math.max(...widths);
- return this.$('.facet-name').css('padding-right', maxValueWidth);
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.components;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(file => file.uuid === key);
- if (item != null) {
- label = item.longName;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
deleted file mode 100644
index d57d9c79cce..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-issue-key-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues);
- },
-
- disable() {
- return this.options.app.state.updateFilter({ issues: null });
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- issues: this.options.app.state.get('query').issues
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
deleted file mode 100644
index 789ae16ca93..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/languages/list';
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- quietMillis: 300,
- url: this.getUrl(),
- data(term) {
- return { q: term, ps: 0 };
- },
- results(data) {
- return {
- more: false,
- results: data.languages.map(lang => {
- return { id: lang.key, text: lang.name };
- })
- };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.languages;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(lang => lang.key === key);
- if (item != null) {
- label = item.name;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
deleted file mode 100644
index b6e87598bff..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import BaseFacet from './base-facet';
-
-export default BaseFacet.extend({
- getValuesWithLabels() {
- const values = this.model.getValues();
- const components = this.options.app.facets.components;
- values.forEach(v => {
- const uuid = v.val;
- let label = uuid;
- if (uuid) {
- const component = components.find(c => c.uuid === uuid);
- if (component != null) {
- label = component.longName;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
deleted file mode 100644
index 9bd37e64b03..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-import Template from '../templates/facets/issues-projects-facet.hbs';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { areThereCustomOrganizations, getOrganization } from '../../../store/organizations/utils';
-
-export default CustomValuesFacet.extend({
- template: Template,
-
- getUrl() {
- return window.baseUrl + '/api/components/search';
- },
-
- prepareSearchForViews() {
- const contextId = this.options.app.state.get('contextComponentUuid');
- return {
- url: window.baseUrl + '/api/components/tree',
- data(term, page) {
- return { q: term, p: page, qualifiers: 'TRK', baseComponentId: contextId };
- }
- };
- },
-
- prepareAjaxSearch() {
- const options = {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { q: term, p: page, qualifiers: 'TRK' };
- },
- results: r => ({
- more: r.paging.total > r.paging.pageIndex * r.paging.pageSize,
- results: r.components.map(component => ({
- id: component.id,
- text: component.name
- }))
- })
- };
- const contextQualifier = this.options.app.state.get('contextComponentQualifier');
- if (contextQualifier === 'VW' || contextQualifier === 'SVW') {
- Object.assign(options, this.prepareSearchForViews());
- }
- return options;
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 3,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 3);
- },
- width: '100%',
- ajax: this.prepareAjaxSearch()
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const projects = this.options.app.facets.components;
- const displayOrganizations = areThereCustomOrganizations();
- values.forEach(v => {
- const uuid = v.val;
- let label = '';
- let organization = null;
- if (uuid) {
- const project = projects.find(p => p.uuid === uuid);
- if (project != null) {
- label = project.longName;
- organization = displayOrganizations && project.organization
- ? getOrganization(project.organization)
- : null;
- }
- }
- v.label = label;
- v.organization = organization;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
deleted file mode 100644
index 3e691aea9be..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/users/search';
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { q: term, p: page };
- },
- results: window.usersToSelect2
- };
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.users;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(user => user.login === key);
- if (item != null) {
- label = item.name;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
deleted file mode 100644
index d4e01b1c0f4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-resolution-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- const value = this.options.app.state.get('query').resolved;
- if (value != null && (!value || value === 'false')) {
- this.$('.js-facet').filter('[data-unresolved]').addClass('active');
- }
- },
-
- toggleFacet(e) {
- const unresolved = $(e.currentTarget).is('[data-unresolved]');
- $(e.currentTarget).toggleClass('active');
- if (unresolved) {
- const checked = $(e.currentTarget).is('.active');
- const value = checked ? 'false' : null;
- return this.options.app.state.updateFilter({
- resolved: value,
- resolutions: null
- });
- } else {
- return this.options.app.state.updateFilter({
- resolved: null,
- resolutions: this.getValue()
- });
- }
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- resolved: null,
- resolutions: null
- });
- },
-
- sortValues(values) {
- const order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
deleted file mode 100644
index 75445570098..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- prepareSearch() {
- let url = window.baseUrl + '/api/rules/search?f=name,langName';
- const languages = this.options.app.state.get('query').languages;
- if (languages != null) {
- url += '&languages=' + languages;
- }
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- url,
- quietMillis: 300,
- data(term, page) {
- return { q: term, p: page };
- },
- results(data) {
- const results = data.rules.map(rule => {
- const lang = rule.langName || translate('manual');
- return {
- id: rule.key,
- text: '(' + lang + ') ' + rule.name
- };
- });
- return {
- more: data.p * data.ps < data.total,
- results
- };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const rules = this.options.app.facets.rules;
- values.forEach(v => {
- const key = v.val;
- let label = '';
- let extra = '';
- if (key) {
- const rule = rules.find(r => r.key === key);
- if (rule != null) {
- label = rule.name;
- }
- if (rule != null) {
- extra = rule.langName;
- }
- }
- v.label = label;
- v.extra = extra;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
deleted file mode 100644
index db5cd337838..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-severity-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
deleted file mode 100644
index cb4e88a5f49..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-status-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
deleted file mode 100644
index 8bb629ff3d4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import CustomValuesFacet from './custom-values-facet';
-import { translate } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- prepareSearch() {
- let url = window.baseUrl + '/api/issues/tags?ps=10';
- const tags = this.options.app.state.get('query').tags;
- if (tags != null) {
- url += '&tags=' + tags;
- }
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 0,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- width: '100%',
- ajax: {
- url,
- quietMillis: 300,
- data(term) {
- return { q: term, ps: 10 };
- },
- results(data) {
- const results = data.tags.map(tag => {
- return { id: tag, text: tag };
- });
- return { more: false, results };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- values.forEach(v => {
- v.label = v.val;
- v.extra = '';
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js
deleted file mode 100644
index efac17ea9f4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-type-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/init.js b/server/sonar-web/src/main/js/apps/issues/init.js
deleted file mode 100644
index 0fb32e839ab..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/init.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import State from './models/state';
-import Layout from './layout';
-import Issues from './models/issues';
-import Facets from '../../components/navigator/models/facets';
-import Controller from './controller';
-import Router from './router';
-import WorkspaceListView from './workspace-list-view';
-import WorkspaceHeaderView from './workspace-header-view';
-import FacetsView from './facets-view';
-import HeaderView from './HeaderView';
-
-const App = new Marionette.Application();
-const init = function({ el, user }) {
- this.state = new State({ user, canBulkChange: user.isLoggedIn });
- this.list = new Issues();
- this.facets = new Facets();
-
- this.layout = new Layout({ app: this, el });
- this.layout.render();
- $('#footer').addClass('search-navigator-footer');
-
- this.controller = new Controller({ app: this });
-
- this.issuesView = new WorkspaceListView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceListRegion.show(this.issuesView);
- this.issuesView.bindScrollEvents();
-
- this.workspaceHeaderView = new WorkspaceHeaderView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
-
- this.facetsView = new FacetsView({
- app: this,
- collection: this.facets
- });
- this.layout.facetsRegion.show(this.facetsView);
-
- this.headerView = new HeaderView({
- app: this
- });
- this.layout.filtersRegion.show(this.headerView);
-
- key.setScope('list');
- App.router = new Router({ app: App });
- Backbone.history.start();
-};
-
-App.on('start', el => {
- init.call(App, el);
-});
-
-export default function(el, user) {
- App.start({ el, user });
-
- return () => {
- Backbone.history.stop();
- App.layout.destroy();
- $('#footer').removeClass('search-navigator-footer');
- };
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js b/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
deleted file mode 100644
index c3f45efbfe9..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import ActionOptionsView from '../../components/common/action-options-view';
-import Template from './templates/issues-issue-filter-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- selectOption(e) {
- const property = $(e.currentTarget).data('property');
- const value = $(e.currentTarget).data('value');
- this.trigger('select', property, value);
- ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- s: this.model.get('severity')
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/layout.js b/server/sonar-web/src/main/js/apps/issues/layout.js
deleted file mode 100644
index c796baccb9c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/layout.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import Template from './templates/issues-layout.hbs';
-import './styles.css';
-
-export default Marionette.LayoutView.extend({
- template: Template,
-
- regions: {
- filtersRegion: '.issues-header',
- facetsRegion: '.search-navigator-facets',
- workspaceHeaderRegion: '.search-navigator-workspace-header',
- workspaceListRegion: '.search-navigator-workspace-list',
- workspaceComponentViewerRegion: '.issues-workspace-component-viewer'
- },
-
- onRender() {
- this.$('.search-navigator').addClass('sticky');
- const top = this.$('.search-navigator').offset().top;
- this.$('.search-navigator-workspace-header').css({ top });
- this.$('.search-navigator-side').css({ top }).isolatedScroll();
- },
-
- showSpinner(region) {
- return this[region].show(
- new Marionette.ItemView({
- template: () => '<i class="spinner"></i>'
- })
- );
- },
-
- showComponentViewer() {
- this.scroll = $(window).scrollTop();
- this.$('.issues').addClass('issues-extended-view');
- },
-
- hideComponentViewer() {
- this.$('.issues').removeClass('issues-extended-view');
- if (this.scroll != null) {
- $(window).scrollTop(this.scroll);
- }
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issue.js b/server/sonar-web/src/main/js/apps/issues/models/issue.js
deleted file mode 100644
index 218e27810ad..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/models/issue.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Issue from '../../../components/issue/models/issue';
-
-export default Issue.extend({
- reset(attrs, options) {
- const keepFields = ['index', 'selected', 'comments'];
- keepFields.forEach(field => {
- attrs[field] = this.get(field);
- });
- return Issue.prototype.reset.call(this, attrs, options);
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issues.js b/server/sonar-web/src/main/js/apps/issues/models/issues.js
deleted file mode 100644
index 9ea6454a484..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/models/issues.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Backbone from 'backbone';
-import Issue from './issue';
-
-export default Backbone.Collection.extend({
- model: Issue,
-
- url() {
- return window.baseUrl + '/api/issues/search';
- },
-
- _injectRelational(issue, source, baseField, lookupField) {
- const baseValue = issue[baseField];
- if (baseValue != null && Array.isArray(source) && source.length > 0) {
- const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
- if (lookupValue != null) {
- Object.keys(lookupValue).forEach(key => {
- const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
- issue[newKey] = lookupValue[key];
- });
- }
- }
- return issue;
- },
-
- _injectCommentsRelational(issue, users) {
- if (issue.comments) {
- const that = this;
- const newComments = issue.comments.map(comment => {
- let newComment = { ...comment, author: comment.login };
- delete newComment.login;
- newComment = that._injectRelational(newComment, users, 'author', 'login');
- return newComment;
- });
- issue = { ...issue, comments: newComments };
- }
- return issue;
- },
-
- _prepareClosed(issue) {
- if (issue.status === 'CLOSED') {
- issue.flows = [];
- delete issue.textRange;
- }
- return issue;
- },
-
- ensureTextRange(issue) {
- if (issue.line && !issue.textRange) {
- // FIXME 999999
- issue.textRange = {
- startLine: issue.line,
- endLine: issue.line,
- startOffset: 0,
- endOffset: 999999
- };
- }
- return issue;
- },
-
- parseIssues(r, startIndex = 0) {
- const that = this;
- return r.issues.map((issue, index) => {
- Object.assign(issue, { index: startIndex + index });
- issue = that._injectRelational(issue, r.components, 'component', 'key');
- issue = that._injectRelational(issue, r.components, 'project', 'key');
- issue = that._injectRelational(issue, r.components, 'subProject', 'key');
- issue = that._injectRelational(issue, r.rules, 'rule', 'key');
- issue = that._injectRelational(issue, r.users, 'assignee', 'login');
- issue = that._injectCommentsRelational(issue, r.users);
- issue = that._prepareClosed(issue);
- issue = that.ensureTextRange(issue);
- return issue;
- });
- },
-
- setIndex() {
- return this.forEach((issue, index) => issue.set({ index }));
- },
-
- selectByKeys(keys) {
- const that = this;
- keys.forEach(key => {
- const issue = that.get(key);
- if (issue) {
- issue.set({ selected: true });
- }
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/state.js b/server/sonar-web/src/main/js/apps/issues/models/state.js
deleted file mode 100644
index b367cc670b0..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/models/state.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import State from '../../../components/navigator/models/state';
-
-export default State.extend({
- defaults: {
- page: 1,
- maxResultsReached: false,
- query: {},
- facets: ['facetMode', 'types', 'resolutions'],
- isContext: false,
- allFacets: [
- 'facetMode',
- 'issues',
- 'types',
- 'resolutions',
- 'severities',
- 'statuses',
- 'createdAt',
- 'rules',
- 'tags',
- 'projectUuids',
- 'moduleUuids',
- 'directories',
- 'fileUuids',
- 'assignees',
- 'authors',
- 'languages'
- ],
- facetsFromServer: [
- 'types',
- 'severities',
- 'statuses',
- 'resolutions',
- 'projectUuids',
- 'directories',
- 'rules',
- 'moduleUuids',
- 'tags',
- 'assignees',
- 'authors',
- 'fileUuids',
- 'languages',
- 'createdAt'
- ],
- transform: {
- resolved: 'resolutions',
- assigned: 'assignees',
- createdBefore: 'createdAt',
- createdAfter: 'createdAt',
- sinceLeakPeriod: 'createdAt',
- createdInLast: 'createdAt'
- }
- },
-
- getFacetMode() {
- const query = this.get('query');
- return query.facetMode || 'count';
- },
-
- toJSON() {
- return { facetMode: this.getFacetMode(), ...this.attributes };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/redirects.js b/server/sonar-web/src/main/js/apps/issues/redirects.js
new file mode 100644
index 00000000000..3efc1af64be
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/redirects.js
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils';
+import type { RawQuery } from './utils';
+
+const parseHash = (hash: string): RawQuery => {
+ const query: RawQuery = {};
+ const parts = hash.split('|');
+ parts.forEach(part => {
+ const tokens = part.split('=');
+ if (tokens.length === 2) {
+ const property = decodeURIComponent(tokens[0]);
+ const value = decodeURIComponent(tokens[1]);
+ if (property === 'assigned_to_me' && value === 'true') {
+ query.myIssues = 'true';
+ } else {
+ query[property] = value;
+ }
+ }
+ });
+ return query;
+};
+
+export const onEnter = (state: Object, replace: Function) => {
+ const { hash } = window.location;
+ if (hash.length > 1) {
+ const query = parseHash(hash.substr(1));
+ const normalizedQuery = {
+ ...serializeQuery(parseQuery(query)),
+ myIssues: areMyIssuesSelected(query) ? 'true' : undefined
+ };
+ replace({
+ pathname: state.location.pathname,
+ query: normalizedQuery
+ });
+ }
+};
diff --git a/server/sonar-web/src/main/js/apps/issues/router.js b/server/sonar-web/src/main/js/apps/issues/router.js
deleted file mode 100644
index ac35322a355..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/router.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Router from '../../components/navigator/router';
-
-export default Router.extend({
- routes: {
- '': 'home',
- ':query': 'index'
- },
-
- home() {
- return this.navigate('resolved=false', { trigger: true, replace: true });
- },
-
- index(query) {
- this.options.app.state.setQuery(this.options.app.controller.parseQuery(query));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/routes.js b/server/sonar-web/src/main/js/apps/issues/routes.js
index 5f3a27c905c..61893ce7fec 100644
--- a/server/sonar-web/src/main/js/apps/issues/routes.js
+++ b/server/sonar-web/src/main/js/apps/issues/routes.js
@@ -17,13 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { onEnter } from './redirects';
+
const routes = [
{
- indexRoute: {
- getComponent(_, callback) {
- require.ensure([], require =>
- callback(null, require('./components/IssuesAppContainer').default));
- }
+ getIndexRoute(_, callback) {
+ require.ensure([], require =>
+ callback(null, {
+ component: require('./components/AppContainer').default,
+ onEnter
+ }));
}
}
];
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
new file mode 100644
index 00000000000..cf7bb4be8f2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchAssignees } from '../utils';
+import type { ReferencedUser, Component } from '../utils';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ assigned: boolean,
+ assignees: Array<string>,
+ component?: Component,
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedUsers: { [string]: ReferencedUser }
+|};
+
+export default class AssigneeFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'assignees';
+
+ handleItemClick = (itemValue: string) => {
+ if (itemValue === '') {
+ // unassigned
+ this.props.onChange({ assigned: !this.props.assigned, assignees: [] });
+ } else {
+ // defined assignee
+ const { assignees } = this.props;
+ const newValue = sortBy(
+ assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue]
+ );
+ this.props.onChange({ assigned: true, assignees: newValue });
+ }
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => searchAssignees(query, this.props.component);
+
+ handleSelect = (assignee: string) => {
+ const { assignees } = this.props;
+ this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) });
+ };
+
+ isAssigneeActive(assignee: string) {
+ return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee);
+ }
+
+ getAssigneeName(assignee: string): React.Element<*> | string {
+ if (assignee === '') {
+ return translate('unassigned');
+ } else {
+ const { referencedUsers } = this.props;
+ if (referencedUsers[assignee]) {
+ return (
+ <span>
+ <Avatar
+ className="little-spacer-right"
+ hash={referencedUsers[assignee].avatar}
+ size={16}
+ />
+ {referencedUsers[assignee].name}
+ </span>
+ );
+ } else {
+ return assignee;
+ }
+ }
+ }
+
+ getStat(assignee: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[assignee] : null;
+ }
+
+ renderOption = (option: { avatar: string, label: string }) => {
+ return (
+ <span>
+ {option.avatar != null &&
+ <Avatar className="little-spacer-right" hash={option.avatar} size={16} />}
+ {option.label}
+ </span>
+ );
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const assignees = sortBy(
+ Object.keys(stats),
+ // put unassigned first
+ key => key === '' ? 0 : 1,
+ // the sort by number
+ key => -stats[key]
+ );
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={!this.props.assigned || this.props.assignees.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {assignees.map(assignee => (
+ <FacetItem
+ active={this.isAssigneeActive(assignee)}
+ facetMode={this.props.facetMode}
+ key={assignee}
+ name={this.getAssigneeName(assignee)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(assignee)}
+ value={assignee}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ renderOption={this.renderOption}
+ />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
new file mode 100644
index 00000000000..d539229ff78
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ authors: Array<string>
+|};
+
+export default class AuthorFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'authors';
+
+ handleItemClick = (itemValue: string) => {
+ const { authors } = this.props;
+ const newValue = sortBy(
+ authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(author: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[author] : null;
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const authors = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.authors.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {authors.map(author => (
+ <FacetItem
+ active={this.props.authors.includes(author)}
+ facetMode={this.props.facetMode}
+ key={author}
+ name={author}
+ onClick={this.handleItemClick}
+ stat={this.getStat(author)}
+ value={author}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
new file mode 100644
index 00000000000..dd28ccfddea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
@@ -0,0 +1,276 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import moment from 'moment';
+import { max } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import { BarChart } from '../../../components/charts/bar-chart';
+import DateInput from '../../../components/controls/DateInput';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import type { Component } from '../utils';
+
+type Props = {|
+ component?: Component,
+ createdAfter: string,
+ createdAt: string,
+ createdBefore: string,
+ createdInLast: string,
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ sinceLeakPeriod: boolean,
+ stats?: { [string]: number }
+|};
+
+const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ';
+
+export default class CreationDateFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'createdAt';
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ resetTo = (changes: {}) => {
+ this.props.onChange({
+ createdAfter: undefined,
+ createdAt: undefined,
+ createdBefore: undefined,
+ createdInLast: undefined,
+ sinceLeakPeriod: undefined,
+ ...changes
+ });
+ };
+
+ handleBarClick = (
+ { createdAfter, createdBefore }: { createdAfter: Object, createdBefore?: Object }
+ ) => {
+ this.resetTo({
+ createdAfter: createdAfter.format(DATE_FORMAT),
+ createdBefore: createdBefore && createdBefore.format(DATE_FORMAT)
+ });
+ };
+
+ handlePeriodChange = (property: string) =>
+ (value: string) => {
+ this.props.onChange({
+ createdAt: undefined,
+ createdInLast: undefined,
+ sinceLeakPeriod: undefined,
+ [property]: value
+ });
+ };
+
+ handlePeriodClick = (period?: string) =>
+ (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur;
+ this.resetTo({ createdInLast: period });
+ };
+
+ handleLeakPeriodClick = () =>
+ (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur;
+ this.resetTo({ sinceLeakPeriod: true });
+ };
+
+ renderBarChart() {
+ const { createdBefore, stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const periods = Object.keys(stats);
+
+ if (periods.length < 2) {
+ return null;
+ }
+
+ const data = periods.map((startDate, index) => {
+ const startMoment = moment(startDate);
+ const nextStartMoment = index < periods.length - 1
+ ? moment(periods[index + 1])
+ : createdBefore ? moment(createdBefore) : undefined;
+ const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days');
+
+ let tooltip = formatMeasure(stats[startDate], 'SHORT_INT') +
+ '<br>' +
+ startMoment.format('LL');
+
+ if (endMoment) {
+ const isSameDay = endMoment.diff(startMoment, 'days') <= 1;
+ if (!isSameDay) {
+ tooltip += ' – ' + endMoment.format('LL');
+ }
+ }
+
+ return {
+ createdAfter: startMoment,
+ createdBefore: nextStartMoment,
+ startMoment,
+ tooltip,
+ x: index,
+ y: stats[startDate]
+ };
+ });
+
+ const barsWidth = Math.floor(240 / data.length);
+ const width = barsWidth * data.length - 1 + 20;
+
+ const maxValue = max(data.map(d => d.y));
+ const format = this.props.facetMode === 'count' ? 'SHORT_INT' : 'SHORT_WORK_DUR';
+ const xValues = data.map(d => d.y === maxValue ? formatMeasure(maxValue, format) : '');
+
+ return (
+ <BarChart
+ barsWidth={barsWidth - 1}
+ data={data}
+ height={75}
+ onBarClick={this.handleBarClick}
+ padding={[25, 10, 5, 10]}
+ width={width}
+ xValues={xValues}
+ />
+ );
+ }
+
+ renderExactDate() {
+ const m = moment(this.props.createdAt);
+ return (
+ <div className="search-navigator-facet-container">
+ {m.format('LLL')}
+ <br />
+ <span className="note">({m.fromNow()})</span>
+ </div>
+ );
+ }
+
+ renderPeriodSelectors() {
+ const { createdAfter, createdBefore } = this.props;
+
+ return (
+ <div className="search-navigator-date-facet-selection">
+ <DateInput
+ className="search-navigator-date-facet-selection-dropdown-left"
+ onChange={this.handlePeriodChange('createdAfter')}
+ placeholder={translate('from')}
+ value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined}
+ />
+ <DateInput
+ className="search-navigator-date-facet-selection-dropdown-right"
+ onChange={this.handlePeriodChange('createdBefore')}
+ placeholder={translate('to')}
+ value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined}
+ />
+ </div>
+ );
+ }
+
+ renderPrefefinedPeriods() {
+ const { component, createdInLast, sinceLeakPeriod } = this.props;
+ return (
+ <div className="spacer-top">
+ <span className="spacer-right">{translate('issues.facet.createdAt.or')}</span>
+ <a className="spacer-right" href="#" onClick={this.handlePeriodClick()}>
+ {translate('issues.facet.createdAt.all')}
+ </a>
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1w' })}
+ href="#"
+ onClick={this.handlePeriodClick('1w')}>
+ {translate('issues.facet.createdAt.last_week')}
+ </a>}
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1m' })}
+ href="#"
+ onClick={this.handlePeriodClick('1m')}>
+ {translate('issues.facet.createdAt.last_month')}
+ </a>}
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1y' })}
+ href="#"
+ onClick={this.handlePeriodClick('1y')}>
+ {translate('issues.facet.createdAt.last_year')}
+ </a>}
+ {component != null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': sinceLeakPeriod })}
+ href="#"
+ onClick={this.handleLeakPeriodClick()}>
+ {translate('issues.leak_period')}
+ </a>}
+ </div>
+ );
+ }
+
+ renderInner() {
+ const { createdAt } = this.props;
+ return createdAt
+ ? this.renderExactDate()
+ : <div>
+ {this.renderBarChart()}
+ {this.renderPeriodSelectors()}
+ {this.renderPrefefinedPeriods()}
+ </div>;
+ }
+
+ render() {
+ const hasValue = this.props.createdAfter.length > 0 ||
+ this.props.createdAt.length > 0 ||
+ this.props.createdBefore.length > 0 ||
+ this.props.createdInLast.length > 0 ||
+ this.props.sinceLeakPeriod;
+
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={hasValue}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open && this.renderInner()}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
new file mode 100644
index 00000000000..ddd82db1eab
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ directories: Array<string>
+|};
+
+export default class DirectoryFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'directories';
+
+ handleItemClick = (itemValue: string) => {
+ const { directories } = this.props;
+ const newValue = sortBy(
+ directories.includes(itemValue)
+ ? without(directories, itemValue)
+ : [...directories, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(directory: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[directory] : null;
+ }
+
+ renderName(directory: string): React.Element<*> | string {
+ // `referencedComponents` are indexed by uuid
+ // so we have to browse them all to find a matching one
+ const { referencedComponents } = this.props;
+ const uuid = Object.keys(referencedComponents).find(
+ uuid => referencedComponents[uuid].key === directory
+ );
+ const name = uuid ? referencedComponents[uuid].name : directory;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="DIR" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const directories = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.directories.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {directories.map(directory => (
+ <FacetItem
+ active={this.props.directories.includes(directory)}
+ facetMode={this.props.facetMode}
+ key={directory}
+ name={this.renderName(directory)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(directory)}
+ value={directory}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
new file mode 100644
index 00000000000..dcfb16202e9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void
+|};
+
+export default class FacetMode extends React.PureComponent {
+ props: Props;
+
+ property = 'facetMode';
+
+ handleItemClick = (itemValue: string) => {
+ this.props.onChange({ [this.property]: itemValue });
+ };
+
+ render() {
+ const { facetMode } = this.props;
+ const modes = ['count', 'effort'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader name={translate('issues.facet.mode')} />
+
+ <FacetItemsList>
+ {modes.map(mode => (
+ <FacetItem
+ active={facetMode === mode}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={mode}
+ name={translate('issues.facet.mode', mode)}
+ onClick={this.handleItemClick}
+ stat={null}
+ value={mode}
+ />
+ ))}
+ </FacetItemsList>
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
new file mode 100644
index 00000000000..5d914f8380d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+import { collapsePath } from '../../../helpers/path';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ files: Array<string>
+|};
+
+export default class FileFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'files';
+
+ handleItemClick = (itemValue: string) => {
+ const { files } = this.props;
+ const newValue = sortBy(
+ files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(file: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[file] : null;
+ }
+
+ renderName(file: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ const name = referencedComponents[file]
+ ? collapsePath(referencedComponents[file].path, 15)
+ : file;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="FIL" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const files = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.files.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {files.map(file => (
+ <FacetItem
+ active={this.props.files.includes(file)}
+ facetMode={this.props.facetMode}
+ key={file}
+ name={this.renderName(file)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(file)}
+ value={file}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
new file mode 100644
index 00000000000..411c9b74d2e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import LanguageFacetFooter from './LanguageFacetFooter';
+import type { ReferencedLanguage } from '../utils';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ languages: Array<string>
+|};
+
+export default class LanguageFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'languages';
+
+ handleItemClick = (itemValue: string) => {
+ const { languages } = this.props;
+ const newValue = sortBy(
+ languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getLanguageName(language: string): string {
+ const { referencedLanguages } = this.props;
+ return referencedLanguages[language] ? referencedLanguages[language].name : language;
+ }
+
+ getStat(language: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[language] : null;
+ }
+
+ handleSelect = (language: string) => {
+ const { languages } = this.props;
+ this.props.onChange({ [this.property]: uniq([...languages, language]) });
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const languages = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.languages.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {languages.map(language => (
+ <FacetItem
+ active={this.props.languages.includes(language)}
+ facetMode={this.props.facetMode}
+ key={language}
+ name={this.getLanguageName(language)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(language)}
+ value={language}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
new file mode 100644
index 00000000000..4ad3bc32825
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { connect } from 'react-redux';
+import { translate } from '../../../helpers/l10n';
+import { getLanguages } from '../../../store/rootReducer';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ languages: Array<{ key: string, name: string }>,
+ onSelect: (value: string) => void
+|};
+
+class LanguageFacetFooter extends React.PureComponent {
+ props: Props;
+
+ handleChange = (option: Option) => {
+ this.props.onSelect(option.value);
+ };
+
+ render() {
+ const options = this.props.languages.map(language => ({
+ label: language.name,
+ value: language.key
+ }));
+
+ return (
+ <div className="search-navigator-facet-footer">
+ <Select
+ autofocus={true}
+ className="input-super-large"
+ clearable={false}
+ noResultsText={translate('select2.noMatches')}
+ onChange={this.handleChange}
+ options={options}
+ placeholder={translate('search_verb')}
+ searchable={true}
+ />
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ languages: Object.values(getLanguages(state))
+});
+
+export default connect(mapStateToProps)(LanguageFacetFooter);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
new file mode 100644
index 00000000000..8711e017462
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ modules: Array<string>
+|};
+
+export default class ModuleFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'modules';
+
+ handleItemClick = (itemValue: string) => {
+ const { modules } = this.props;
+ const newValue = sortBy(
+ modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(module: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[module] : null;
+ }
+
+ renderName(module: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ const name = referencedComponents[module] ? referencedComponents[module].name : module;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const modules = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.modules.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {modules.map(module => (
+ <FacetItem
+ active={this.props.modules.includes(module)}
+ facetMode={this.props.facetMode}
+ key={module}
+ name={this.renderName(module)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(module)}
+ value={module}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
new file mode 100644
index 00000000000..2b7046f69ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import type { ReferencedComponent, Component } from '../utils';
+import Organization from '../../../components/shared/Organization';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { searchComponents, getTree } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ component?: Component,
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ projects: Array<string>
+|};
+
+export default class ProjectFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'projects';
+
+ handleItemClick = (itemValue: string) => {
+ const { projects } = this.props;
+ const newValue = sortBy(
+ projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ const { component } = this.props;
+
+ return component != null && ['VW', 'SVW'].includes(component.qualifier)
+ ? getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
+ response.components.map(component => ({
+ label: component.name,
+ organization: component.organization,
+ value: component.refId
+ })))
+ : searchComponents({ ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
+ response.components.map(component => ({
+ label: component.name,
+ organization: component.organization,
+ value: component.id
+ })));
+ };
+
+ handleSelect = (rule: string) => {
+ const { projects } = this.props;
+ this.props.onChange({ [this.property]: uniq([...projects, rule]) });
+ };
+
+ getStat(project: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[project] : null;
+ }
+
+ renderName(project: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ return referencedComponents[project]
+ ? <span>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ <Organization link={false} organizationKey={referencedComponents[project].organization} />
+ {referencedComponents[project].name}
+ </span>
+ : <span>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {project}
+ </span>;
+ }
+
+ renderOption = (option: { label: string, organization: string }) => {
+ return (
+ <span>
+ <Organization link={false} organizationKey={option.organization} />
+ {option.label}
+ </span>
+ );
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const projects = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.projects.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {projects.map(project => (
+ <FacetItem
+ active={this.props.projects.includes(project)}
+ facetMode={this.props.facetMode}
+ key={project}
+ name={this.renderName(project)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(project)}
+ value={project}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter
+ minimumQueryLength={3}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ renderOption={this.renderOption}
+ />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
new file mode 100644
index 00000000000..d83c56cd917
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ resolved: boolean,
+ resolutions: Array<string>,
+ stats?: { [string]: number }
+|};
+
+export default class ResolutionFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'resolutions';
+
+ handleItemClick = (itemValue: string) => {
+ if (itemValue === '') {
+ // unresolved
+ this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
+ } else {
+ // defined resolution
+ const { resolutions } = this.props;
+ const newValue = orderBy(
+ resolutions.includes(itemValue)
+ ? without(resolutions, itemValue)
+ : [...resolutions, itemValue]
+ );
+ this.props.onChange({ resolved: true, resolutions: newValue });
+ }
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ isFacetItemActive(resolution: string) {
+ return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution);
+ }
+
+ getFacetItemName(resolution: string) {
+ return resolution === '' ? translate('unresolved') : translate('issue.resolution', resolution);
+ }
+
+ getStat(resolution: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[resolution] : null;
+ }
+
+ renderItem = (resolution: string) => {
+ const active = this.isFacetItemActive(resolution);
+ const stat = this.getStat(resolution);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ key={resolution}
+ halfWidth={true}
+ name={this.getFacetItemName(resolution)}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={resolution}
+ />
+ );
+ };
+
+ render() {
+ const resolutions = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={!this.props.resolved || this.props.resolutions.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {resolutions.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
new file mode 100644
index 00000000000..ce5d5d49468
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchRules } from '../../../api/rules';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ languages: Array<string>,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedRules: { [string]: { name: string } },
+ rules: Array<string>
+|};
+
+export default class RuleFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'rules';
+
+ handleItemClick = (itemValue: string) => {
+ const { rules } = this.props;
+ const newValue = sortBy(
+ rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ const { languages } = this.props;
+ return searchRules({
+ f: 'name,langName',
+ languages: languages.length ? languages.join() : undefined,
+ q: query
+ }).then(response =>
+ response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key })));
+ };
+
+ handleSelect = (rule: string) => {
+ const { rules } = this.props;
+ this.props.onChange({ [this.property]: uniq([...rules, rule]) });
+ };
+
+ getRuleName(rule: string): string {
+ const { referencedRules } = this.props;
+ return referencedRules[rule] ? referencedRules[rule].name : rule;
+ }
+
+ getStat(rule: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[rule] : null;
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const rules = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.rules.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {rules.map(rule => (
+ <FacetItem
+ active={this.props.rules.includes(rule)}
+ facetMode={this.props.facetMode}
+ key={rule}
+ name={this.getRuleName(rule)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(rule)}
+ value={rule}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
index cadec2974e3..e95f44008d0 100644
--- a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
-import { orderBy, uniq, without } from 'lodash';
+import { orderBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
@@ -28,6 +28,7 @@ import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate } from '../../../helpers/l10n';
type Props = {|
+ facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
@@ -47,9 +48,7 @@ export default class SeverityFacet extends React.PureComponent {
handleItemClick = (itemValue: string) => {
const { severities } = this.props;
const newValue = orderBy(
- severities.includes(itemValue)
- ? without(severities, itemValue)
- : uniq([...severities, itemValue])
+ severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};
@@ -63,6 +62,25 @@ export default class SeverityFacet extends React.PureComponent {
return stats ? stats[severity] : null;
}
+ renderItem = (severity: string) => {
+ const active = this.props.severities.includes(severity);
+ const stat = this.getStat(severity);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={severity}
+ name={<SeverityHelper severity={severity} />}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={severity}
+ />
+ );
+ };
+
render() {
const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
@@ -75,19 +93,10 @@ export default class SeverityFacet extends React.PureComponent {
open={this.props.open}
/>
- <FacetItemsList open={this.props.open}>
- {severities.map(severity => (
- <FacetItem
- active={this.props.severities.includes(severity)}
- halfWidth={true}
- key={severity}
- name={<SeverityHelper severity={severity} />}
- onClick={this.handleItemClick}
- stat={this.getStat(severity)}
- value={severity}
- />
- ))}
- </FacetItemsList>
+ {this.props.open &&
+ <FacetItemsList>
+ {severities.map(this.renderItem)}
+ </FacetItemsList>}
</FacetBox>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
new file mode 100644
index 00000000000..fd4c2c3f91c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
@@ -0,0 +1,212 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import AssigneeFacet from './AssigneeFacet';
+import AuthorFacet from './AuthorFacet';
+import CreationDateFacet from './CreationDateFacet';
+import DirectoryFacet from './DirectoryFacet';
+import FacetMode from './FacetMode';
+import FileFacet from './FileFacet';
+import LanguageFacet from './LanguageFacet';
+import ModuleFacet from './ModuleFacet';
+import ProjectFacet from './ProjectFacet';
+import ResolutionFacet from './ResolutionFacet';
+import RuleFacet from './RuleFacet';
+import SeverityFacet from './SeverityFacet';
+import StatusFacet from './StatusFacet';
+import TagFacet from './TagFacet';
+import TypeFacet from './TypeFacet';
+import type {
+ Query,
+ Facet,
+ ReferencedComponent,
+ ReferencedUser,
+ ReferencedLanguage,
+ Component
+} from '../utils';
+
+type Props = {|
+ component?: Component,
+ facets: { [string]: Facet },
+ myIssues: boolean,
+ onFacetToggle: (property: string) => void,
+ onFilterChange: (changes: { [string]: Array<string> }) => void,
+ openFacets: { [string]: boolean },
+ query: Query,
+ referencedComponents: { [string]: ReferencedComponent },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ referencedRules: { [string]: { name: string } },
+ referencedUsers: { [string]: ReferencedUser }
+|};
+
+export default class Sidebar extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { component, facets, openFacets, query } = this.props;
+
+ const displayProjectsFacet: boolean = component == null ||
+ !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
+ const displayModulesFacet = component == null || component.qualifier !== 'DIR';
+ const displayDirectoriesFacet = component == null || component.qualifier !== 'DIR';
+ const displayAuthorFacet = component == null || component.qualifier !== 'DEV';
+
+ return (
+ <div className="search-navigator-facets-list">
+ <FacetMode facetMode={query.facetMode} onChange={this.props.onFilterChange} />
+ <TypeFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.types}
+ stats={facets.types}
+ types={query.types}
+ />
+ <ResolutionFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.resolutions}
+ resolved={query.resolved}
+ resolutions={query.resolutions}
+ stats={facets.resolutions}
+ />
+ <SeverityFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.severities}
+ severities={query.severities}
+ stats={facets.severities}
+ />
+ <StatusFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.statuses}
+ stats={facets.statuses}
+ statuses={query.statuses}
+ />
+ <CreationDateFacet
+ component={component}
+ createdAfter={query.createdAfter}
+ createdAt={query.createdAt}
+ createdBefore={query.createdBefore}
+ createdInLast={query.createdInLast}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.createdAt}
+ sinceLeakPeriod={query.sinceLeakPeriod}
+ stats={facets.createdAt}
+ />
+ <RuleFacet
+ facetMode={query.facetMode}
+ languages={query.languages}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.rules}
+ stats={facets.rules}
+ referencedRules={this.props.referencedRules}
+ rules={query.rules}
+ />
+ <TagFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.tags}
+ stats={facets.tags}
+ tags={query.tags}
+ />
+ {displayProjectsFacet &&
+ <ProjectFacet
+ component={component}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.projects}
+ projects={query.projects}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.projects}
+ />}
+ {displayModulesFacet &&
+ <ModuleFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.modules}
+ modules={query.modules}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.modules}
+ />}
+ {displayDirectoriesFacet &&
+ <DirectoryFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.directories}
+ directories={query.directories}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.directories}
+ />}
+ <FileFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.files}
+ files={query.files}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.files}
+ />
+ {!this.props.myIssues &&
+ <AssigneeFacet
+ component={component}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.assignees}
+ assigned={query.assigned}
+ assignees={query.assignees}
+ referencedUsers={this.props.referencedUsers}
+ stats={facets.assignees}
+ />}
+ {displayAuthorFacet &&
+ <AuthorFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.authors}
+ authors={query.authors}
+ stats={facets.authors}
+ />}
+ <LanguageFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.languages}
+ languages={query.languages}
+ referencedLanguages={this.props.referencedLanguages}
+ stats={facets.languages}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
new file mode 100644
index 00000000000..40bfb251141
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ statuses: Array<string>
+|};
+
+export default class StatusFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'statuses';
+
+ handleItemClick = (itemValue: string) => {
+ const { statuses } = this.props;
+ const newValue = orderBy(
+ statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(status: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[status] : null;
+ }
+
+ renderStatus(status: string) {
+ return (
+ <span>
+ <i className={`icon-status-${status.toLowerCase()}`} />
+ {' '}
+ {translate('issue.status', status)}
+ </span>
+ );
+ }
+
+ renderItem = (status: string) => {
+ const active = this.props.statuses.includes(status);
+ const stat = this.getStat(status);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={status}
+ name={this.renderStatus(status)}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={status}
+ />
+ );
+ };
+
+ render() {
+ const statuses = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.statuses.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {statuses.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
new file mode 100644
index 00000000000..cd6ab46e679
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchIssueTags } from '../../../api/issues';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ tags: Array<string>
+|};
+
+export default class TagFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'tags';
+
+ handleItemClick = (itemValue: string) => {
+ const { tags } = this.props;
+ const newValue = sortBy(
+ tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ return searchIssueTags({ ps: 50, q: query }).then(tags =>
+ tags.map(tag => ({ label: tag, value: tag })));
+ };
+
+ handleSelect = (tag: string) => {
+ const { tags } = this.props;
+ this.props.onChange({ [this.property]: uniq([...tags, tag]) });
+ };
+
+ getStat(tag: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[tag] : null;
+ }
+
+ renderTag(tag: string) {
+ return (
+ <span>
+ <i className="icon-tags icon-gray little-spacer-right" />
+ {tag}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const tags = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.tags.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {tags.map(tag => (
+ <FacetItem
+ active={this.props.tags.includes(tag)}
+ facetMode={this.props.facetMode}
+ key={tag}
+ name={this.renderTag(tag)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(tag)}
+ value={tag}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
new file mode 100644
index 00000000000..c0eb0271058
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ types: Array<string>
+|};
+
+export default class TypeFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'types';
+
+ handleItemClick = (itemValue: string) => {
+ const { types } = this.props;
+ const newValue = orderBy(
+ types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(type: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[type] : null;
+ }
+
+ renderItem = (type: string) => {
+ const active = this.props.types.includes(type);
+ const stat = this.getStat(type);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ key={type}
+ name={<span><IssueTypeIcon query={type} /> {translate('issue.type', type)}</span>}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={type}
+ />
+ );
+ };
+
+ render() {
+ const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.types.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {types.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
new file mode 100644
index 00000000000..5dc2230f4e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import AssigneeFacet from '../AssigneeFacet';
+
+jest.mock('../../../../store/rootReducer', () => ({}));
+
+const renderAssigneeFacet = (props?: {}) =>
+ shallow(
+ <AssigneeFacet
+ assigned={true}
+ assignees={[]}
+ facetMode="count"
+ onChange={jest.fn()}
+ onToggle={jest.fn()}
+ open={true}
+ referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }}
+ stats={{ '': 5, foo: 13, bar: 7 }}
+ {...props}
+ />
+ );
+
+it('should render', () => {
+ expect(renderAssigneeFacet()).toMatchSnapshot();
+});
+
+it('should not render without stats', () => {
+ expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot();
+});
+
+it('should select unassigned', () => {
+ expect(renderAssigneeFacet({ assigned: false })).toMatchSnapshot();
+});
+
+it('should select user', () => {
+ expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot();
+});
+
+it('should render footer select option', () => {
+ const wrapper = renderAssigneeFacet();
+ expect(
+ wrapper.instance().renderOption({ avatar: 'avatar-foo', label: 'name-foo' })
+ ).toMatchSnapshot();
+});
+
+it('should call onChange', () => {
+ const onChange = jest.fn();
+ const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
+ const itemOnClick = wrapper.find('FacetItem').first().prop('onClick');
+
+ itemOnClick('');
+ expect(onChange).lastCalledWith({ assigned: false, assignees: [] });
+
+ itemOnClick('bar');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar', 'foo'] });
+
+ itemOnClick('foo');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: [] });
+});
+
+it('should call onToggle', () => {
+ const onToggle = jest.fn();
+ const wrapper = renderAssigneeFacet({ onToggle });
+ const headerOnClick = wrapper.find('FacetHeader').prop('onClick');
+
+ headerOnClick();
+ expect(onToggle).lastCalledWith('assignees');
+});
+
+it('should handle footer callbacks', () => {
+ const onChange = jest.fn();
+ const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
+ const onSelect = wrapper.find('FacetFooter').prop('onSelect');
+
+ onSelect('qux');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] });
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
new file mode 100644
index 00000000000..a55c903b90c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
+
+jest.mock('../../../../store/rootReducer', () => ({}));
+
+const renderSidebar = props =>
+ shallow(<Sidebar facets={{}} myIssues={false} openFacets={{}} query={{}} {...props} />)
+ .children()
+ .map(node => node.name());
+
+it('should render all facets', () => {
+ expect(renderSidebar()).toMatchSnapshot();
+});
+
+it('should render facets for project', () => {
+ expect(renderSidebar({ component: { qualifier: 'TRK' } })).toMatchSnapshot();
+});
+
+it('should render facets for module', () => {
+ expect(renderSidebar({ component: { qualifier: 'BRC' } })).toMatchSnapshot();
+});
+
+it('should render facets for directory', () => {
+ expect(renderSidebar({ component: { qualifier: 'DIR' } })).toMatchSnapshot();
+});
+
+it('should render facets for developer', () => {
+ expect(renderSidebar({ component: { qualifier: 'DEV' } })).toMatchSnapshot();
+});
+
+it('should render facets when my issues are selected', () => {
+ expect(renderSidebar({ myIssues: true })).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
new file mode 100644
index 00000000000..80726a28b0b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
@@ -0,0 +1,167 @@
+exports[`test should not render without stats 1`] = `null`;
+
+exports[`test should render 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={false}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
+
+exports[`test should render footer select option 1`] = `
+<span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatar-foo"
+ size={16} />
+ name-foo
+</span>
+`;
+
+exports[`test should select unassigned 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={true}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={true}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
+
+exports[`test should select user 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={true}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={true}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
new file mode 100644
index 00000000000..81d6ce875fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
@@ -0,0 +1,112 @@
+exports[`test should render all facets 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for developer 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for directory 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for module 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for project 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets when my issues are selected 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js
new file mode 100644
index 00000000000..358f6ee3a19
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+type Props = {|
+ children?: React.Element<*>,
+ property: string
+|};
+
+const FacetBox = (props: Props) => (
+ <div className="search-navigator-facet-box" data-property={props.property}>
+ {props.children}
+ </div>
+);
+
+export default FacetBox;
diff --git a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js
index 67d71fe37cc..8f0fca20fdc 100644
--- a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js
@@ -18,19 +18,26 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import { connect } from 'react-redux';
-import BaseIssue from './BaseIssue';
-import { getIssueByKey } from '../../store/rootReducer';
-import { onFail } from '../../store/rootActions';
-import { updateIssue } from './actions';
+import React from 'react';
+import SearchSelect from '../../components/SearchSelect';
-const mapStateToProps = (state, ownProps) => ({
- issue: getIssueByKey(state, ownProps.issueKey)
-});
+type Option = { label: string, value: string };
-const mapDispatchToProps = {
- onIssueChange: updateIssue,
- onFail: error => dispatch => onFail(dispatch)(error)
-};
+type Props = {|
+ minimumQueryLength?: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>
+|};
-export default connect(mapStateToProps, mapDispatchToProps)(BaseIssue);
+export default class FacetFooter extends React.PureComponent {
+ props: Props;
+
+ render() {
+ return (
+ <div className="search-navigator-facet-footer">
+ <SearchSelect {...this.props} />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
new file mode 100644
index 00000000000..ff6ef8387c9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+/* eslint-disable max-len */
+import React from 'react';
+
+type Props = {
+ hasValue: boolean,
+ name: string,
+ onClick?: () => void,
+ open: boolean
+};
+
+export default class FacetHeader extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ hasValue: false,
+ open: true
+ };
+
+ handleClick = (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ if (this.props.onClick) {
+ this.props.onClick();
+ }
+ };
+
+ renderCheckbox() {
+ return (
+ <svg viewBox="0 0 1792 1792" width="10" height="10" style={{ paddingTop: 3 }}>
+ {this.props.open
+ ? <path
+ style={{ fill: 'currentColor ' }}
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ />
+ : <path
+ style={{ fill: 'currentColor ' }}
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ />}
+ </svg>
+ );
+ }
+
+ renderValueIndicator() {
+ return this.props.hasValue && !this.props.open
+ ? <svg viewBox="0 0 1792 1792" width="8" height="8" style={{ paddingTop: 5, paddingLeft: 8 }}>
+ <path
+ d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
+ fill="#4b9fd5"
+ />
+ </svg>
+ : null;
+ }
+
+ render() {
+ return this.props.onClick
+ ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}>
+ {this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()}
+ </a>
+ : <span className="search-navigator-facet-header">
+ {this.props.name}
+ </span>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js
new file mode 100644
index 00000000000..94d512d195b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import { formatMeasure } from '../../../../helpers/measures';
+
+type Props = {|
+ active: boolean,
+ disabled: boolean,
+ facetMode: string,
+ halfWidth: boolean,
+ name: string | React.Element<*>,
+ onClick: (string) => void,
+ stat: ?number,
+ value: string
+|};
+
+export default class FacetItem extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ disabled: false,
+ halfWidth: false
+ };
+
+ handleClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ const value = event.currentTarget.dataset.value;
+ this.props.onClick(value);
+ };
+
+ render() {
+ const { stat } = this.props;
+
+ const className = classNames('facet', 'search-navigator-facet', {
+ active: this.props.active,
+ 'search-navigator-facet-half': this.props.halfWidth
+ });
+
+ const formattedStat = stat &&
+ formatMeasure(stat, this.props.facetMode === 'effort' ? 'SHORT_WORK_DUR' : 'SHORT_INT');
+
+ return this.props.disabled
+ ? <span className={className}>
+ <span className="facet-name">{this.props.name}</span>
+ <span className="facet-stat">{formattedStat}</span>
+ </span>
+ : <a className={className} data-value={this.props.value} href="#" onClick={this.handleClick}>
+ <span className="facet-name">{this.props.name}</span>
+ <span className="facet-stat">{formattedStat}</span>
+ </a>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-issues/routes.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js
index 954ba1487bb..4a203f0c071 100644
--- a/server/sonar-web/src/main/js/apps/component-issues/routes.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js
@@ -17,15 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-const routes = [
- {
- indexRoute: {
- getComponent(_, callback) {
- require.ensure([], require =>
- callback(null, require('./components/ComponentIssuesAppContainer').default));
- }
- }
- }
-];
+// @flow
+import React from 'react';
-export default routes;
+type Props = {|
+ children?: Array<React.Element<*>>
+|};
+
+const FacetItemsList = (props: Props) => (
+ <div className="search-navigator-facet-list">
+ {props.children}
+ </div>
+);
+
+export default FacetItemsList;
diff --git a/server/sonar-web/src/main/js/components/issue/models/changelog.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js
index bf1150a310a..0eb453e8066 100644
--- a/server/sonar-web/src/main/js/components/issue/models/changelog.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js
@@ -17,14 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetBox from '../FacetBox';
-export default Backbone.Collection.extend({
- url() {
- return window.baseUrl + '/api/issues/changelog';
- },
-
- parse(r) {
- return r.changelog;
- }
+it('should render', () => {
+ expect(shallow(<FacetBox property="foo"><div /></FacetBox>)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/shared/qualifier-icon.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js
index d3e8aead051..4dbf1cc3ece 100644
--- a/server/sonar-web/src/main/js/components/shared/qualifier-icon.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js
@@ -17,14 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+// @flow
import React from 'react';
+import { shallow } from 'enzyme';
+import FacetFooter from '../FacetFooter';
-export default React.createClass({
- render() {
- if (!this.props.qualifier) {
- return null;
- }
- const className = 'icon-qualifier-' + this.props.qualifier.toLowerCase();
- return <i className={className} />;
- }
+it('should render', () => {
+ expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
new file mode 100644
index 00000000000..5aa00c4a41e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import FacetHeader from '../FacetHeader';
+
+it('should render open facet with value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render open facet without value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render closed facet with value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={false} />)
+ ).toMatchSnapshot();
+});
+
+it('should render closed facet without value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />)
+ ).toMatchSnapshot();
+});
+
+it('should render without link', () => {
+ expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(
+ <FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} />
+ );
+ click(wrapper);
+ expect(onClick).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js
new file mode 100644
index 00000000000..ddc84eca458
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import FacetItem from '../FacetItem';
+
+const renderFacetItem = (props: {}) =>
+ shallow(
+ <FacetItem
+ active={false}
+ facetMode="count"
+ name="foo"
+ onClick={jest.fn()}
+ stat={null}
+ value="bar"
+ {...props}
+ />
+ );
+
+it('should render active', () => {
+ expect(renderFacetItem({ active: true })).toMatchSnapshot();
+});
+
+it('should render inactive', () => {
+ expect(renderFacetItem({ active: false })).toMatchSnapshot();
+});
+
+it('should render stat', () => {
+ expect(renderFacetItem({ stat: 13 })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+ expect(renderFacetItem({ disabled: true })).toMatchSnapshot();
+});
+
+it('should render half width', () => {
+ expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot();
+});
+
+it('should render effort stat', () => {
+ expect(renderFacetItem({ facetMode: 'effort', stat: 1234 })).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = renderFacetItem({ onClick });
+ click(wrapper, { currentTarget: { dataset: { value: 'bar' } } });
+ expect(onClick).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js
index c65b1c7a8d0..883e62b88d4 100644
--- a/server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js
@@ -17,6 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-module.exports = function(id) {
- return window.baseUrl + '/issues/search#id=' + id;
-};
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetItemsList from '../FacetItemsList';
+
+it('should render', () => {
+ expect(shallow(<FacetItemsList><div /></FacetItemsList>)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap
new file mode 100644
index 00000000000..2722912ca7d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap
@@ -0,0 +1,7 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-box"
+ data-property="foo">
+ <div />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap
new file mode 100644
index 00000000000..21f8096c8e8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap
@@ -0,0 +1,10 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-footer">
+ <SearchSelect
+ minimumQueryLength={2}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ resetOnBlur={true} />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
new file mode 100644
index 00000000000..3333ae8944d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
@@ -0,0 +1,132 @@
+exports[`test should render closed facet with value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+ <svg
+ height="8"
+ style={
+ Object {
+ "paddingLeft": 8,
+ "paddingTop": 5,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="8">
+ <path
+ d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
+ fill="#4b9fd5" />
+ </svg>
+</a>
+`;
+
+exports[`test should render closed facet without value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render open facet with value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render open facet without value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render without link 1`] = `
+<span
+ className="search-navigator-facet-header">
+ foo
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap
new file mode 100644
index 00000000000..0a22b710499
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap
@@ -0,0 +1,90 @@
+exports[`test should render active 1`] = `
+<a
+ className="facet search-navigator-facet active"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render disabled 1`] = `
+<span
+ className="facet search-navigator-facet">
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</span>
+`;
+
+exports[`test should render effort stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat">
+ work_duration.x_days.3
+ </span>
+</a>
+`;
+
+exports[`test should render half width 1`] = `
+<a
+ className="facet search-navigator-facet search-navigator-facet-half"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render inactive 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat">
+ 13
+ </span>
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap
new file mode 100644
index 00000000000..15686ac81d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap
@@ -0,0 +1,6 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-list">
+ <div />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
deleted file mode 100644
index 46942bb2796..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.issues-header-inner {
- padding: 32px 10px 15px;
- background-color: #f3f3f3;
- text-align: center;
-}
-
-.issues-header-inner:empty {
- display: none;
-}
-
-.issues-header-order {
- display: inline-block;
- vertical-align: top;
- margin-right: 20px;
- font-size: 12px;
-}
-
-.issues-header-order {
- display: inline-block;
- vertical-align: top;
- margin-right: 20px;
- font-size: 12px;
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs b/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs
deleted file mode 100644
index fda291ab2b3..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs
+++ /dev/null
@@ -1,145 +0,0 @@
-{{#if isLoaded}}
- <form id="bulk-change-form">
- <div class="modal-head">
- <h2>{{tp 'issue_bulk_change.form.title' issues.length}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
-
- {{#if limitReached}}
- <div class="alert alert-warning">
- {{tp 'issue_bulk_change.max_issues_reached' issues.length}}
- </div>
- {{/if}}
-
- {{! assign }}
- {{#if canBeAssigned}}
- <div class="modal-field">
- <label for="assignee">{{t 'issue.assign.formlink'}}</label>
- <input id="assign-action" name="actions[]" type="checkbox" value="assign">
- <input id="assignee" type="hidden">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canBeAssigned}})
- </div>
- </div>
- {{/if}}
-
- {{! type }}
- {{#if canChangeType}}
- <div class="modal-field">
- <label for="type">{{t 'issue.set_type'}}</label>
- <input id="set-type-action" name="actions[]" type="checkbox" value="set_type">
- <select id="type" name="set_type.type">
- <option value="BUG">{{t 'issue.type.BUG'}}</option>
- <option value="VULNERABILITY">{{t 'issue.type.VULNERABILITY'}}</option>
- <option value="CODE_SMELL">{{t 'issue.type.CODE_SMELL'}}</option>
- </select>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeType}})
- </div>
- </div>
- {{/if}}
-
- {{! severity }}
- {{#if canChangeSeverity}}
- <div class="modal-field">
- <label for="severity">{{t 'issue.set_severity'}}</label>
- <input id="set-severity-action" name="actions[]" type="checkbox" value="set_severity">
- <select id="severity" name="set_severity.severity">
- <option value="BLOCKER">{{t 'severity.BLOCKER'}}</option>
- <option value="CRITICAL">{{t 'severity.CRITICAL'}}</option>
- <option value="MAJOR">{{t 'severity.MAJOR'}}</option>
- <option value="MINOR">{{t 'severity.MINOR'}}</option>
- <option value="INFO">{{t 'severity.INFO'}}</option>
- </select>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeSeverity}})
- </div>
- </div>
- {{/if}}
-
- {{! add tags }}
- {{#if canChangeTags}}
- <div class="modal-field">
- <label for="add_tags">{{t 'issue.add_tags'}}</label>
- <input id="add-tags-action" name="actions[]" type="checkbox" value="add_tags">
- <input id="add_tags" name="add_tags.tags" type="text">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
- </div>
- </div>
- {{/if}}
-
- {{! remove tags }}
- {{#if canChangeTags}}
- <div class="modal-field">
- <label for="remove_tags">{{t 'issue.remove_tags'}}</label>
- <input id="remove-tags-action" name="actions[]" type="checkbox" value="remove_tags">
- <input id="remove_tags" name="remove_tags.tags" type="text">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
- </div>
- </div>
- {{/if}}
-
- {{! transitions }}
- {{#notEmpty availableTransitions}}
- <div class="modal-field">
- <label>{{t 'issue.transition'}}</label>
- {{#each availableTransitions}}
- <input type="radio" id="transition-{{transition}}" name="do_transition.transition"
- value="{{transition}}">
- <label for="transition-{{transition}}" style="float: none; display: inline; left: 0; cursor: pointer;">
- {{t 'issue.transition' transition}}
- </label>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' count}})
- </div>
- <br>
- {{/each}}
- </div>
- {{/notEmpty}}
-
- {{! comment }}
- {{#if canBeCommented}}
- <div class="modal-field">
- <label for="comment">
- {{t 'issue.comment.formlink'}}
- <i class="icon-help" title="{{t 'issue_bulk_change.comment.help'}}"></i>
- </label>
- <div>
- <textarea rows="4" name="comment" id="comment" style="width: 100%"></textarea>
- </div>
- <div class="pull-right">
- {{> ../../../components/common/templates/_markdown-tips}}
- </div>
- </div>
- {{/if}}
-
- {{! notifications }}
- <div class="modal-field">
- <label for="send-notifications">{{t 'issue.send_notifications'}}</label>
- <input id="send-notifications" name="sendNotifications" type="checkbox" value="true">
- </div>
-
- </div>
- <div class="modal-foot">
- <i class="js-modal-spinner spinner spacer-right hidden"></i>
- <button id="bulk-change-submit">{{t 'apply'}}</button>
- <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a>
- </div>
- </form>
-{{else}}
- <div class="modal-head">
- <h2>{{t 'bulk_change'}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
- <div class="text-center">
- <i class="spinner spinner-margin"></i>
- </div>
- </div>
- <div class="modal-foot">
- <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a>
- </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
deleted file mode 100644
index 64237bc0e79..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
+++ /dev/null
@@ -1,4 +0,0 @@
-<a class="search-navigator-facet-header js-facet-toggle">
- <i class="icon-checkbox {{#if enabled}}icon-checkbox-checked{{/if}}"></i>
- {{t "issues.facet" property}}
-</a>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
deleted file mode 100644
index 9fb23f78ab8..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
+++ /dev/null
@@ -1,26 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- {{#eq val ""}}
- {{! unassigned }}
- <a class="facet search-navigator-facet js-facet" data-unassigned title="{{t "unassigned"}}">
- <span class="facet-name">{{t "unassigned"}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../../state.facetMode}}
- </span>
- </a>
- {{else}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{label}}">
- <span class="facet-name">{{label}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../../state.facetMode}}
- </span>
- </a>
- {{/eq}}
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
deleted file mode 100644
index 83442f81b37..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-{{> "_issues-facet-header"}}
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../state.facetMode}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
deleted file mode 100644
index 9f981c07c1a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="search-navigator-facet-query">
- Issues of &nbsp;&nbsp; {{qualifierIcon state.contextComponentQualifier}}&nbsp;{{state.contextComponentName}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
deleted file mode 100644
index bf5410833c9..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
+++ /dev/null
@@ -1,42 +0,0 @@
-{{> "_issues-facet-header"}}
-
-{{#if createdAt}}
- <input type="hidden" name="createdAt">
- <div class="search-navigator-facet-container">
- {{dt createdAt}} ({{fromNow createdAt}})
- </div>
-{{else}}
- <div class="search-navigator-facet-container">
- <div class="js-barchart" data-height="75" {{#if periodEnd}}data-end-date="{{periodEnd}}"{{/if}}></div>
- <div class="search-navigator-date-facet-selection">
- <a class="js-select-period-start search-navigator-date-facet-selection-dropdown-left">
- {{#if periodStart}}{{d periodStart}}{{else}}Past{{/if}}&nbsp;<i class="icon-dropdown"></i>
- </a>
- <a class="js-select-period-end search-navigator-date-facet-selection-dropdown-right">
- {{#if periodEnd}}{{d periodEnd}}{{else}}Now{{/if}}&nbsp;<i class="icon-dropdown"></i>
- </a>
- <input class="js-period-start search-navigator-date-facet-selection-input-left"
- type="text" value="{{#if periodStart}}{{ds periodStart}}{{/if}}" name="createdAfter">
- <input class="js-period-end search-navigator-date-facet-selection-input-right"
- type="text" value="{{#if periodEnd}}{{ds periodEnd}}{{/if}}" name="createdBefore">
- </div>
-
- <div class="spacer-top">
- <span class="spacer-right">{{t "issues.facet.createdAt.or"}}</span>
- <a class="js-all spacer-right" href="#">{{t "issues.facet.createdAt.all"}}</a>
- {{#if hasLeak}}
- <a class="js-leak spacer-right {{#eq sinceLeakPeriod "true"}}active-link{{/eq}}" href="#">Leak Period</a>
- {{else}}
- <a class="js-last-week spacer-right {{#eq createdInLast "1w"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_week"}}
- </a>
- <a class="js-last-month spacer-right {{#eq createdInLast "1m"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_month"}}
- </a>
- <a class="js-last-year {{#eq createdInLast "1y"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_year"}}
- </a>
- {{/if}}
- </div>
- </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
deleted file mode 100644
index 0674f5b87bb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
deleted file mode 100644
index 3569040b69c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list search-navigator-facet-list-align-right">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
deleted file mode 100644
index 28ae140f75b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
+++ /dev/null
@@ -1,7 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-container">
- <div class="facet search-navigator-facet active" style="cursor: default;">
- <span class="facet-name">{{issues}}</span>
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs
deleted file mode 100644
index af69286d92a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="search-navigator-facet-header">
- {{t 'issues.facet.mode'}}
-</div>
-
-
-<div class="search-navigator-facet-list">
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'count'}}active{{/eq}}"
- data-value="count">
- <span class="facet-name">{{t 'issues.facet.mode.issues'}}</span>
- </a>
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'effort'}}active{{/eq}}"
- data-value="effort">
- <span class="facet-name">{{t 'issues.facet.mode.effort'}}</span>
- </a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs
deleted file mode 100644
index c11181a70dd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-{{#if user.isLoggedIn}}
- <ul class="radio-toggle">
- <li>
- <input type="radio" name="issues-page-my" value="my" id="issues-page-my-my" {{#if me}}checked{{/if}}>
- <label for="issues-page-my-my">My Issues</label>
- </li>
- <li>
- <input type="radio" name="issues-page-my" value="all" id="issues-page-my-all" {{#unless me}}checked{{/unless}}>
- <label for="issues-page-my-all">All</label>
- </li>
- </ul>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs
deleted file mode 100644
index 7693af65549..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}">
- <span class="facet-name">{{#if organization}}{{organization.name}}<span class="slash-separator"></span>{{/if}}{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
deleted file mode 100644
index 19e47071f8b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- {{#eq val ""}}
- {{! unresolved }}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-unresolved
- title="{{t "issue.unresolved.description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{t "unresolved"}}</span>
- <span class="facet-stat">
- {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{else}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-value="{{val}}"
- title="{{t "issue.resolution" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{t "issue.resolution" val}}</span>
- <span class="facet-stat">
- {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/eq}}
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
deleted file mode 100644
index 88b9bd0585b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet"
- data-value="{{val}}" title="{{t "severity" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{severityIcon val}} {{t "severity" val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
deleted file mode 100644
index cc7f1fcefdb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet"
- data-value="{{val}}" title="{{t "issue.status" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{statusIcon val}} {{t "issue.status" val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs
deleted file mode 100644
index f97ada41dca..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet"
- data-value="{{val}}">
- <span class="facet-name">{{issueTypeIcon val}} {{t 'issue.type' val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
deleted file mode 100644
index f18f6845d3a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
+++ /dev/null
@@ -1,89 +0,0 @@
-<header class="menu-search">
- <h6>{{t "issue.filter_similar_issues"}}</h6>
-</header>
-
-<ul class="menu">
- <li>
- <a href="#" data-property="types" data-value="{{this.type}}">
- {{issueType this.type}}
- </a>
- </li>
-
- <li>
- <a href="#" data-property="severities" data-value="{{s}}">
- {{severityIcon severity}}&nbsp;{{t "severity" severity}}
- </a>
- </li>
-
- <li>
- <a href="#" data-property="statuses" data-value="{{status}}">
- {{statusIcon status}}&nbsp;{{t "issue.status" status}}
- </a>
- </li>
-
- <li>
- {{#if resolution}}
- <a href="#" data-property="resolutions" data-value="{{resolution}}">
- {{t "issue.resolution" resolution}}
- </a>
- {{else}}
- <a href="#" data-property="resolved" data-value="false">
- {{t "unresolved"}}
- </a>
- {{/if}}
- </li>
-
- <li>
- {{#if assignee}}
- <a href="#" class="issue-action-option" data-property="assignees" data-value="{{assignee}}">
- {{t "assigned_to"}}
- {{#ifShowAvatars}}<span class="spacer-left">{{avatarHelperNew assigneeAvatar 16}}</span>{{/ifShowAvatars}}
- {{assigneeName}}
- </a>
- {{else}}
- <a href="#" data-property="assigned" data-value="false">
- {{t "unassigned"}}
- </a>
- {{/if}}
- </li>
-
- <li class="divider"></li>
-
- <li>
- <a href="#" data-property="rules" data-value="{{rule}}">
- {{limitString ruleName}}
- </a>
- </li>
-
- {{#each tags}}
- <li>
- <a href="#" data-property="tags" data-value="{{this}}">
- <i class="icon-tags icon-half-transparent"></i>&nbsp;{{this}}
- </a>
- </li>
- {{/each}}
-
- <li class="divider"></li>
-
- <li>
- <a href="#" data-property="projectUuids" data-value="{{projectUuid}}">
- {{qualifierIcon "TRK"}}&nbsp;{{projectLongName}}
- </a>
- </li>
-
- {{#if subProject}}
- <li>
- <a href="#" data-property="moduleUuids" data-value="{{subProjectUuid}}">
- {{qualifierIcon "BRC"}}&nbsp;{{subProjectLongName}}
- </a>
- </li>
- {{/if}}
-
- <li>
- <a href="#" data-property="fileUuids" data-value="{{componentUuid}}">
- {{qualifierIcon componentQualifier}}&nbsp;{{fileFromPath componentLongName}}
- </a>
- </li>
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
deleted file mode 100644
index 83add61b366..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-<div class="issues search-navigator">
- <div class="search-navigator-side">
- <div class="issues-header"></div>
- <div class="search-navigator-facets"></div>
- </div>
-
- <div class="search-navigator-workspace">
- <div class="search-navigator-workspace-header"></div>
- <div class="search-navigator-workspace-list"></div>
- <div class="issues-workspace-component-viewer"></div>
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
deleted file mode 100644
index 6a28b925ad8..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
+++ /dev/null
@@ -1,80 +0,0 @@
-<div class="issues-header-component nowrap">
- <div class="component-name">
- {{#if state.component}}
-
- <div class="component-name-parent">
- <a class="js-back">{{t "issues.return_to_list"}}</a>&nbsp;&nbsp;&nbsp;
- </div>
-
- <div class="component-name-parent">
- {{#if organization}}
- <a href="{{link '/organizations/' organization.key}}">{{organization.name}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- {{#with state.component}}
- {{#if project}}
- <a href="{{dashboardUrl project}}" title="{{projectName}}">{{projectName}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- {{#if subProject}}
- <a href="{{dashboardUrl subProject}}" title="{{subProjectName}}">{{subProjectName}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- <a href="{{dashboardUrl key}}" title="{{name}}">{{collapsePath name}}</a>
- {{/with}}
- </div>
-
- {{else}}
- {{#if state.canBulkChange}}
- <a class="js-selection icon-checkbox {{#if allSelected}}icon-checkbox-checked{{/if}} {{#if someSelected}}icon-checkbox-checked icon-checkbox-single{{/if}}"
- data-toggle="tooltip" title="{{t 'issues.toggle_selection_tooltip'}}"></a>
- {{else}}
- &nbsp;
- {{/if}}
- {{/if}}
- </div>
-</div>
-
-
-<div class="search-navigator-header-actions">
- {{#notNull state.total}}
- {{#unless state.component}}
- <div class="issues-header-order">{{t 'issues.ordered'}} <strong>{{t 'issues.by_creation_date'}}</strong></div>
- {{/unless}}
- <div class="search-navigator-header-pagination flash flash-heavy">
- {{#gt state.total 0}}
- <strong>
- <span class="current">
- {{sum state.selectedIndex 1}}
- /
- <span id="issues-total">{{formatMeasure state.total 'INT'}}</span>
- </span>
- </strong>
- {{else}}
- <span class="current">0 / <span id="issues-total">0</span></span>
- {{/gt}}
- {{t 'issues.issues'}}
- </div>
- {{/notNull}}
-
-
- <div class="search-navigator-header-buttons button-group dropdown">
- <button id="issues-reload" class="js-reload">{{t "reload"}}</button>
- <button class="js-new-search" id="issues-new-search">{{t "issue_filter.new_search"}}</button>
- {{#if state.canBulkChange}}
- <button id="issues-bulk-change" class="dropdown-toggle" data-toggle="dropdown">
- {{t "bulk_change"}}
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a class="js-bulk-change" href="#">{{tp 'issues.bulk_change' state.total}}</a>
- </li>
- {{#gt selectedCount 0}}
- <li>
- <a class="js-bulk-change-selected" href="#">{{tp 'issues.bulk_change_selected' selectedCount}}</a>
- </li>
- {{/gt}}
- </ul>
- {{/if}}
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
deleted file mode 100644
index b0e305ecddb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
+++ /dev/null
@@ -1,26 +0,0 @@
-<div class="component-name issues-workspace-list-component">
- {{#if organization}}
- <a class="link-no-underline" href="{{link '/organizations/' organization.key}}">
- {{organization.name}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- {{#if project}}
- <a class="link-no-underline" href="{{dashboardUrl project}}">
- {{projectLongName}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- {{#if subProject}}
- <a class="link-no-underline" href="{{dashboardUrl subProject}}">
- {{subProjectLongName}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- <a class="link-no-underline" href="{{dashboardUrl component}}">
- {{collapsePath componentLongName}}
- </a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
deleted file mode 100644
index cae9c964a0b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
+++ /dev/null
@@ -1,5 +0,0 @@
-<div class="js-list"></div>
-
-<div class="search-navigator-workspace-list-more">
- <span class="js-more"><i class="spinner"></i></span>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js
new file mode 100644
index 00000000000..3c9d8a235ed
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/utils.js
@@ -0,0 +1,229 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { isNil, omitBy } from 'lodash';
+import { searchMembers } from '../../api/organizations';
+import { searchUsers } from '../../api/users';
+
+export type RawQuery = { [string]: string };
+
+export type Query = {|
+ assigned: boolean,
+ assignees: Array<string>,
+ authors: Array<string>,
+ createdAfter: string,
+ createdAt: string,
+ createdBefore: string,
+ createdInLast: string,
+ directories: Array<string>,
+ facetMode: string,
+ files: Array<string>,
+ issues: Array<string>,
+ languages: Array<string>,
+ modules: Array<string>,
+ projects: Array<string>,
+ resolved: boolean,
+ resolutions: Array<string>,
+ rules: Array<string>,
+ severities: Array<string>,
+ sinceLeakPeriod: boolean,
+ statuses: Array<string>,
+ tags: Array<string>,
+ types: Array<string>
+|};
+
+export type Paging = {
+ pageIndex: number,
+ pageSize: number,
+ total: number
+};
+
+const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean =>
+ value === 'false' ? false : value === 'true' ? true : defaultValue;
+
+const parseAsString = (value: ?string): string => value || '';
+
+const parseAsStringArray = (value: ?string): Array<string> => value ? value.split(',') : [];
+
+const parseAsFacetMode = (facetMode: string) =>
+ facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count';
+
+export const parseQuery = (query: RawQuery): Query => ({
+ assigned: parseAsBoolean(query.assigned),
+ assignees: parseAsStringArray(query.assignees),
+ authors: parseAsStringArray(query.authors),
+ createdAfter: parseAsString(query.createdAfter),
+ createdAt: parseAsString(query.createdAt),
+ createdBefore: parseAsString(query.createdBefore),
+ createdInLast: parseAsString(query.createdInLast),
+ directories: parseAsStringArray(query.directories),
+ facetMode: parseAsFacetMode(query.facetMode),
+ files: parseAsStringArray(query.fileUuids),
+ issues: parseAsStringArray(query.issues),
+ languages: parseAsStringArray(query.languages),
+ modules: parseAsStringArray(query.moduleUuids),
+ projects: parseAsStringArray(query.projectUuids),
+ resolved: parseAsBoolean(query.resolved),
+ resolutions: parseAsStringArray(query.resolutions),
+ rules: parseAsStringArray(query.rules),
+ severities: parseAsStringArray(query.severities),
+ sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
+ statuses: parseAsStringArray(query.statuses),
+ tags: parseAsStringArray(query.tags),
+ types: parseAsStringArray(query.types)
+});
+
+export const getOpen = (query: RawQuery) => query.open;
+
+export const areMyIssuesSelected = (query: RawQuery): boolean => query.myIssues === 'true';
+
+const serializeString = (value: string): ?string => value || undefined;
+
+const serializeValue = (value: Array<string>): ?string => value.length ? value.join() : undefined;
+
+export const serializeQuery = (query: Query): RawQuery => {
+ const filter = {
+ assigned: query.assigned ? undefined : 'false',
+ assignees: serializeValue(query.assignees),
+ authors: serializeValue(query.authors),
+ createdAfter: serializeString(query.createdAfter),
+ createdAt: serializeString(query.createdAt),
+ createdBefore: serializeString(query.createdBefore),
+ createdInLast: serializeString(query.createdInLast),
+ directories: serializeValue(query.directories),
+ facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined,
+ fileUuids: serializeValue(query.files),
+ issues: serializeValue(query.issues),
+ languages: serializeValue(query.languages),
+ moduleUuids: serializeValue(query.modules),
+ projectUuids: serializeValue(query.projects),
+ resolved: query.resolved ? undefined : 'false',
+ resolutions: serializeValue(query.resolutions),
+ severities: serializeValue(query.severities),
+ sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
+ statuses: serializeValue(query.statuses),
+ rules: serializeValue(query.rules),
+ tags: serializeValue(query.tags),
+ types: serializeValue(query.types)
+ };
+ return omitBy(filter, isNil);
+};
+
+const areArraysEqual = (a: Array<string>, b: Array<string>) => {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export const areQueriesEqual = (a: RawQuery, b: RawQuery) => {
+ const parsedA: Query = parseQuery(a);
+ const parsedB: Query = parseQuery(b);
+
+ const keysA = Object.keys(parsedA);
+ const keysB = Object.keys(parsedB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ return keysA.every(
+ key =>
+ Array.isArray(parsedA[key]) && Array.isArray(parsedB[key])
+ ? areArraysEqual(parsedA[key], parsedB[key])
+ : parsedA[key] === parsedB[key]
+ );
+};
+
+type RawFacet = {
+ property: string,
+ values: Array<{ val: string, count: number }>
+};
+
+export type Facet = { [string]: number };
+
+export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => {
+ // for readability purpose
+ const propertyMapping = {
+ fileUuids: 'files',
+ moduleUuids: 'modules',
+ projectUuids: 'projects'
+ };
+
+ const result = {};
+ facets.forEach(facet => {
+ const values = {};
+ facet.values.forEach(value => {
+ values[value.val] = value.count;
+ });
+ const finalProperty = propertyMapping[facet.property] || facet.property;
+ result[finalProperty] = values;
+ });
+ return result;
+};
+
+export type ReferencedComponent = {
+ key: string,
+ name: string,
+ organization: string,
+ path: string
+};
+
+export type ReferencedUser = {
+ avatar: string,
+ name: string
+};
+
+export type ReferencedLanguage = {
+ name: string
+};
+
+export type Component = {
+ key: string,
+ organization: string,
+ qualifier: string
+};
+
+export type CurrentUser =
+ | { isLoggedIn: false }
+ | { isLoggedIn: true, email?: string, login: string, name: string };
+
+export const searchAssignees = (query: string, component?: Component) => {
+ return component
+ ? searchMembers({ organization: component.organization, ps: 50, q: query }).then(response =>
+ response.users.map(user => ({
+ avatar: user.avatar,
+ label: user.name,
+ value: user.login
+ })))
+ : searchUsers(query, 50).then(response =>
+ response.users.map(user => ({
+ // TODO this WS returns no avatar
+ avatar: user.avatar,
+ email: user.email,
+ label: user.name,
+ value: user.login
+ })));
+};
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
deleted file mode 100644
index 0fccbb3ab65..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import WorkspaceHeaderView from '../../components/navigator/workspace-header-view';
-import BulkChangeForm from './BulkChangeForm';
-import Template from './templates/issues-workspace-header.hbs';
-import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils';
-
-export default WorkspaceHeaderView.extend({
- template: Template,
-
- initialize() {
- this.context = {
- isContext: this.options.app.state.get('isContext'),
- organization: this.options.app.state.get('contextOrganization')
- };
- },
-
- events() {
- return {
- ...WorkspaceHeaderView.prototype.events.apply(this, arguments),
- 'click .js-selection': 'onSelectionClick',
- 'click .js-back': 'returnToList',
- 'click .js-new-search': 'newSearch',
- 'click .js-bulk-change-selected': 'onBulkChangeSelectedClick'
- };
- },
-
- onSelectionClick(e) {
- e.preventDefault();
- this.toggleSelection();
- },
-
- onBulkChangeSelectedClick(e) {
- e.preventDefault();
- this.bulkChangeSelected();
- },
-
- afterBulkChange() {
- const selectedIndex = this.options.app.state.get('selectedIndex');
- const selectedKeys = this.options.app.list.where({ selected: true }).map(item => item.id);
- this.options.app.controller.fetchList().done(() => {
- this.options.app.state.set({ selectedIndex });
- this.options.app.list.selectByKeys(selectedKeys);
- });
- },
-
- render() {
- if (!this._suppressUpdate) {
- WorkspaceHeaderView.prototype.render.apply(this, arguments);
- }
- },
-
- toggleSelection() {
- this._suppressUpdate = true;
- const selectedCount = this.options.app.list.where({ selected: true }).length;
- const someSelected = selectedCount > 0;
- return someSelected ? this.selectNone() : this.selectAll();
- },
-
- selectNone() {
- this.options.app.list.where({ selected: true }).forEach(issue => {
- issue.set({ selected: false });
- });
- this._suppressUpdate = false;
- this.render();
- },
-
- selectAll() {
- this.options.app.list.forEach(issue => {
- issue.set({ selected: true });
- });
- this._suppressUpdate = false;
- this.render();
- },
-
- returnToList() {
- this.options.app.controller.closeComponentViewer();
- },
-
- newSearch() {
- this.options.app.controller.newSearch();
- },
-
- bulkChange() {
- const query = this.options.app.controller.getQueryAsObject();
- new BulkChangeForm({
- query,
- context: this.context,
- onChange: () => this.afterBulkChange()
- }).render();
- },
-
- bulkChangeSelected() {
- const selected = this.options.app.list.where({ selected: true });
- const selectedKeys = selected.map(item => item.id).slice(0, 500);
- const query = { issues: selectedKeys.join() };
- new BulkChangeForm({
- query,
- context: this.context,
- onChange: () => this.afterBulkChange()
- }).render();
- },
-
- serializeData() {
- const issuesCount = this.options.app.list.length;
- const selectedCount = this.options.app.list.where({ selected: true }).length;
- const allSelected = issuesCount > 0 && issuesCount === selectedCount;
- const someSelected = !allSelected && selectedCount > 0;
- const data = {
- ...WorkspaceHeaderView.prototype.serializeData.apply(this, arguments),
- selectedCount,
- allSelected,
- someSelected
- };
- const component = this.options.app.state.get('component');
- if (component) {
- const qualifier = this.options.app.state.get('contextComponentQualifier');
- if (qualifier === 'VW' || qualifier === 'SVW') {
- // do nothing
- } else if (qualifier === 'TRK') {
- data.state.component.project = null;
- } else if (qualifier === 'BRC') {
- data.state.component.project = null;
- data.state.component.subProject = null;
- } else {
- const organization = areThereCustomOrganizations()
- ? getOrganization(component.projectOrganization)
- : null;
- Object.assign(data, { organization });
- }
- }
- return data;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
deleted file mode 100644
index ec59af550c0..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import React from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-import Marionette from 'backbone.marionette';
-import ConnectedIssue from '../../components/issue/ConnectedIssue';
-import IssueFilterView from './issue-filter-view';
-import WithStore from '../../components/shared/WithStore';
-import getStore from '../../app/utils/getStore';
-import { getIssueByKey } from '../../store/rootReducer';
-
-const SHOULD_NULL = {
- any: ['issues'],
- resolutions: ['resolved'],
- resolved: ['resolutions'],
- assignees: ['assigned'],
- assigned: ['assignees']
-};
-
-export default Marionette.ItemView.extend({
- className: 'issues-workspace-list-item',
-
- initialize(options) {
- this.openComponentViewer = this.openComponentViewer.bind(this);
- this.onIssueFilterClick = this.onIssueFilterClick.bind(this);
- this.onIssueCheck = this.onIssueCheck.bind(this);
- this.listenTo(options.app.state, 'change:selectedIndex', this.showIssue);
- this.listenTo(this.model, 'change:selected', this.showIssue);
- this.subscribeToStore();
- },
-
- template() {
- return '<div></div>';
- },
-
- subscribeToStore() {
- const store = getStore();
- store.subscribe(() => {
- const issue = getIssueByKey(store.getState(), this.model.get('key'));
- this.model.set(issue);
- });
- },
-
- onRender() {
- this.showIssue();
- },
-
- onDestroy() {
- unmountComponentAtNode(this.el);
- },
-
- showIssue() {
- const selected = this.model.get('index') === this.options.app.state.get('selectedIndex');
-
- render(
- <WithStore>
- <ConnectedIssue
- issueKey={this.model.get('key')}
- checked={this.model.get('selected')}
- onCheck={this.onIssueCheck}
- onClick={this.openComponentViewer}
- onFilterClick={this.onIssueFilterClick}
- selected={selected}
- />
- </WithStore>,
- this.el
- );
- },
-
- onIssueFilterClick(e) {
- const that = this;
- e.preventDefault();
- e.stopPropagation();
- $('body').click();
- this.popup = new IssueFilterView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: this.model
- });
- this.popup.on('select', (property, value) => {
- const obj = {};
- obj[property] = '' + value;
- SHOULD_NULL.any.forEach(p => {
- obj[p] = null;
- });
- if (SHOULD_NULL[property] != null) {
- SHOULD_NULL[property].forEach(p => {
- obj[p] = null;
- });
- }
- that.options.app.state.updateFilter(obj);
- that.popup.destroy();
- });
- this.popup.render();
- },
-
- onIssueCheck(e) {
- e.preventDefault();
- e.stopPropagation();
- this.model.set({ selected: !this.model.get('selected') });
- const selected = this.model.collection.where({ selected: true }).length;
- this.options.app.state.set({ selected });
- },
-
- changeSelection() {
- const selected = this.model.get('index') === this.options.app.state.get('selectedIndex');
- if (selected) {
- this.select();
- } else {
- this.unselect();
- }
- },
-
- selectCurrent() {
- this.options.app.state.set({ selectedIndex: this.model.get('index') });
- },
-
- resetIssue(options) {
- const that = this;
- const key = this.model.get('key');
- const componentUuid = this.model.get('componentUuid');
- const index = this.model.get('index');
- const selected = this.model.get('selected');
- this.model.reset(
- {
- key,
- componentUuid,
- index,
- selected
- },
- { silent: true }
- );
- return this.model.fetch(options).done(() => that.trigger('reset'));
- },
-
- openComponentViewer() {
- this.options.app.state.set({ selectedIndex: this.model.get('index') });
- if (this.options.app.state.has('component')) {
- return this.options.app.controller.closeComponentViewer();
- } else {
- return this.options.app.controller.showComponentViewer(this.model);
- }
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
deleted file mode 100644
index f74b7c912b2..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import WorkspaceListView from '../../components/navigator/workspace-list-view';
-import IssueView from './workspace-list-item-view';
-import EmptyView from './workspace-list-empty-view';
-import Template from './templates/issues-workspace-list.hbs';
-import ComponentTemplate from './templates/issues-workspace-list-component.hbs';
-import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils';
-
-const COMPONENT_HEIGHT = 29;
-const BOTTOM_OFFSET = 60;
-
-export default WorkspaceListView.extend({
- template: Template,
- componentTemplate: ComponentTemplate,
- childView: IssueView,
- childViewContainer: '.js-list',
- emptyView: EmptyView,
-
- bindShortcuts() {
- const that = this;
- WorkspaceListView.prototype.bindShortcuts.apply(this, arguments);
- key('right', 'list', () => {
- const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
- that.options.app.controller.showComponentViewer(selectedIssue);
- return false;
- });
- key('space', 'list', () => {
- const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
- selectedIssue.set({ selected: !selectedIssue.get('selected') });
- return false;
- });
- },
-
- unbindShortcuts() {
- WorkspaceListView.prototype.unbindShortcuts.apply(this, arguments);
- key.unbind('right', 'list');
- key.unbind('space', 'list');
- },
-
- scrollTo() {
- const selectedIssue = this.collection.at(this.options.app.state.get('selectedIndex'));
- if (selectedIssue == null) {
- return;
- }
- const selectedIssueView = this.children.findByModel(selectedIssue);
- const parentTopOffset = this.$el.offset().top;
- let viewTop = selectedIssueView.$el.offset().top - parentTopOffset;
- if (selectedIssueView.$el.prev().is('.issues-workspace-list-component')) {
- viewTop -= COMPONENT_HEIGHT;
- }
- const viewBottom = selectedIssueView.$el.offset().top +
- selectedIssueView.$el.outerHeight() +
- BOTTOM_OFFSET;
- const windowTop = $(window).scrollTop();
- const windowBottom = windowTop + $(window).height();
- if (viewTop < windowTop) {
- $(window).scrollTop(viewTop);
- }
- if (viewBottom > windowBottom) {
- $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom);
- }
- },
-
- attachHtml(compositeView, childView, index) {
- const container = this.getChildViewContainer(compositeView);
- const model = this.collection.at(index);
- if (model != null) {
- const prev = index > 0 && this.collection.at(index - 1);
- let putComponent = !prev;
- if (prev) {
- const fullComponent = [model.get('project'), model.get('component')].join(' ');
- const fullPrevComponent = [prev.get('project'), prev.get('component')].join(' ');
- if (fullComponent !== fullPrevComponent) {
- putComponent = true;
- }
- }
- if (putComponent) {
- this.displayComponent(container, model);
- }
- }
- container.append(childView.el);
- },
-
- displayComponent(container, model) {
- const data = { ...model.toJSON() };
- const qualifier = this.options.app.state.get('contextComponentQualifier');
- if (qualifier === 'VW' || qualifier === 'SVW') {
- Object.assign(data, { organization: undefined });
- } else if (qualifier === 'TRK') {
- Object.assign(data, { organization: undefined, project: undefined });
- } else if (qualifier === 'BRC') {
- Object.assign(data, { organization: undefined, project: undefined, subProject: undefined });
- } else {
- const organization = areThereCustomOrganizations()
- ? getOrganization(model.get('projectOrganization'))
- : null;
- Object.assign(data, { organization });
- }
- container.append(this.componentTemplate(data));
- },
-
- destroyChildren() {
- WorkspaceListView.prototype.destroyChildren.apply(this, arguments);
- this.$('.issues-workspace-list-component').remove();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
index bf1b7d89862..1fba6f72119 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
@@ -51,7 +51,7 @@ class OrganizationFavoriteProjects extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
<FavoriteProjectsContainer
location={this.props.location}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
index 77eabf062da..67dbdf8bcd4 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
@@ -51,7 +51,7 @@ class OrganizationProjects extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
<AllProjectsContainer
isFavorite={false}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
index d04badd1faf..b29c17b3a5c 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
@@ -47,7 +47,18 @@ exports[`test new_reliability_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "sinceLeakPeriod": "true",
+ "types": "BUG",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -91,7 +102,18 @@ exports[`test new_security_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "sinceLeakPeriod": "true",
+ "types": "VULNERABILITY",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -135,7 +157,17 @@ exports[`test new_sqale_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "sinceLeakPeriod": "true",
+ "types": "CODE_SMELL",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -223,7 +255,17 @@ exports[`test reliability_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "types": "BUG",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -267,7 +309,17 @@ exports[`test security_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "types": "VULNERABILITY",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -311,7 +363,16 @@ exports[`test sqale_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "types": "CODE_SMELL",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
index 2d97ab9b996..9fd4ffc417e 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
@@ -22,7 +22,7 @@ import { Link } from 'react-router';
import { difference } from 'lodash';
import Backbone from 'backbone';
import { PermissionTemplateType, CallbackType } from '../propTypes';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import UpdateView from '../views/UpdateView';
import DeleteView from '../views/DeleteView';
import { translate } from '../../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
index 7928cb05f4c..bb722731c4f 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import UpdateKeyForm from './UpdateKeyForm';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
export default class FineGrainedUpdate extends React.Component {
render() {
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projects-admin/projects.js
index 88c0940368d..f3984b0cbdd 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/projects.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/projects.js
@@ -23,7 +23,7 @@ import { Link } from 'react-router';
import { getComponentPermissionsUrl } from '../../helpers/urls';
import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
import Checkbox from '../../components/controls/Checkbox';
-import QualifierIcon from '../../components/shared/qualifier-icon';
+import QualifierIcon from '../../components/shared/QualifierIcon';
import { translate } from '../../helpers/l10n';
export default class Projects extends React.Component {
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
index ec5299b31a8..c62d9e8c612 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
@@ -24,6 +24,11 @@ import ProjectsListFooterContainer from './ProjectsListFooterContainer';
import PageSidebar from './PageSidebar';
import VisualizationsContainer from '../visualizations/VisualizationsContainer';
import { parseUrlQuery } from '../store/utils';
+import Page from '../../../components/layout/Page';
+import PageMain from '../../../components/layout/PageMain';
+import PageMainInner from '../../../components/layout/PageMainInner';
+import PageSide from '../../../components/layout/PageSide';
+import PageFilters from '../../../components/layout/PageFilters';
import '../styles.css';
export default class AllProjects extends React.Component {
@@ -95,38 +100,41 @@ export default class AllProjects extends React.Component {
const top = this.props.organization ? 95 : 30;
return (
- <div className="page-with-sidebar page-with-left-sidebar projects-page">
- <aside className="page-sidebar-fixed page-sidebar-sticky projects-sidebar">
- <div className="page-sidebar-sticky-inner" style={{ top }}>
+ <Page className="projects-page">
+ <PageSide top={top}>
+ <PageFilters>
<PageSidebar
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
- </div>
- </aside>
- <div className="page-main">
- <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
- {view === 'list' &&
- <ProjectsListContainer
- isFavorite={this.props.isFavorite}
- isFiltered={isFiltered}
- organization={this.props.organization}
- />}
- {view === 'list' &&
- <ProjectsListFooterContainer
- query={query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}
- />}
- {view === 'visualizations' &&
- <VisualizationsContainer
- onVisualizationChange={this.handleVisualizationChange}
- sort={query.sort}
- visualization={visualization}
- />}
- </div>
- </div>
+ </PageFilters>
+ </PageSide>
+
+ <PageMain>
+ <PageMainInner>
+ <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
+ {view === 'list' &&
+ <ProjectsListContainer
+ isFavorite={this.props.isFavorite}
+ isFiltered={isFiltered}
+ organization={this.props.organization}
+ />}
+ {view === 'list' &&
+ <ProjectsListFooterContainer
+ query={query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ />}
+ {view === 'visualizations' &&
+ <VisualizationsContainer
+ onVisualizationChange={this.handleVisualizationChange}
+ sort={query.sort}
+ visualization={visualization}
+ />}
+ </PageMainInner>
+ </PageMain>
+ </Page>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js
index c5c3b94bda1..69c18380bb3 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/App.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/App.js
@@ -32,7 +32,7 @@ export default class App extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
{this.props.children}
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
index ed924559a85..d102a9dcef4 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
@@ -55,14 +55,14 @@ export default class PageSidebar extends React.PureComponent {
: undefined;
return (
- <div className="search-navigator-facets-list">
+ <div>
<FavoriteFilterContainer organization={this.props.organization} />
<div className="projects-facets-header clearfix">
{isFiltered &&
<div className="projects-facets-reset">
<Link to={{ pathname, query: linkQuery }} className="button button-red">
- {translate('projects.clear_all_filters')}
+ {translate('clear_all_filters')}
</Link>
</div>}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
index 1c4cae090f2..d692e2ca9ce 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
@@ -19,9 +19,9 @@
*/
import React from 'react';
import ProjectCardContainer from './ProjectCardContainer';
-import NoProjects from './NoProjects';
import NoFavoriteProjects from './NoFavoriteProjects';
import EmptyInstance from './EmptyInstance';
+import EmptySearch from '../../../components/common/EmptySearch';
export default class ProjectsList extends React.PureComponent {
static propTypes = {
@@ -37,7 +37,7 @@ export default class ProjectsList extends React.PureComponent {
} else if (!this.props.isFiltered) {
return <EmptyInstance />;
} else {
- return <NoProjects />;
+ return <EmptySearch />;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
index 211b0fbafb1..faf96ac7d0a 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
@@ -16,7 +16,7 @@ exports[`test should handle \`view\` and \`visualization\` 2`] = `
},
}
}>
- projects.clear_all_filters
+ clear_all_filters
</Link>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css
index 45f8cd2663b..99e3232f7d0 100644
--- a/server/sonar-web/src/main/js/apps/projects/styles.css
+++ b/server/sonar-web/src/main/js/apps/projects/styles.css
@@ -10,14 +10,6 @@
margin-bottom: 0;
}
-.projects-empty-list {
- padding: 60px 0;
- border: 1px solid #e6e6e6;
- border-radius: 2px;
- text-align: center;
- color: #777;
-}
-
.project-card {
position: relative;
min-height: 121px;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
index 6d877d785fa..171d4ab8e0d 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import ChangeProjectsView from '../views/ChangeProjectsView';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import { getProfileProjects } from '../../../api/quality-profiles';
import { translate } from '../../../helpers/l10n';
import type { Profile } from '../propTypes';
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
index 0acd2dc123d..41517e76d50 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
@@ -21,7 +21,6 @@
import { connect } from 'react-redux';
import SourceViewerBase from './SourceViewerBase';
import { receiveFavorites } from '../../store/favorites/duck';
-import { receiveIssues } from '../../store/issues/duck';
const mapStateToProps = null;
@@ -39,11 +38,6 @@ const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean
}
};
-const onReceiveIssues = (issues: Array<*>) =>
- dispatch => {
- dispatch(receiveIssues(issues));
- };
-
-const mapDispatchToProps = { onReceiveComponent, onReceiveIssues };
+const mapDispatchToProps = { onReceiveComponent };
export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
index c0cae1208f8..2e66388c3a1 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -70,10 +70,10 @@ type Props = {
loadIssues: (string, number, number) => Promise<*>,
loadSources: (string, number, number) => Promise<*>,
onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void,
+ onIssueChange?: (Issue) => void,
onIssueSelect?: (string) => void,
onIssueUnselect?: () => void,
onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void,
- onReceiveIssues: (issues: Array<*>) => void,
selectedIssue?: string
};
@@ -93,7 +93,7 @@ type State = {
highlightedLine: number | null,
highlightedSymbols: Array<string>,
issues?: Array<Issue>,
- issuesByLine: { [number]: Array<string> },
+ issuesByLine: { [number]: Array<Issue> },
issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
@@ -221,10 +221,8 @@ export default class SourceViewerBase extends React.Component {
fetchComponent() {
this.setState({ loading: true });
-
const loadIssues = (component, sources) => {
this.props.loadIssues(this.props.component, 1, LINES).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
const finalSources = sources.slice(0, LINES);
this.setState(
@@ -329,7 +327,6 @@ export default class SourceViewerBase extends React.Component {
const from = Math.max(1, firstSourceLine.line - LINES);
this.props.loadSources(this.props.component, from, firstSourceLine.line - 1).then(sources => {
this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
this.setState(prevState => ({
issues: uniqBy([...issues, ...prevState.issues], issue => issue.key),
@@ -353,7 +350,6 @@ export default class SourceViewerBase extends React.Component {
const toLine = lastSourceLine.line + LINES + 1;
this.props.loadSources(this.props.component, fromLine, toLine).then(sources => {
this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
this.setState(prevState => ({
issues: uniqBy([...prevState.issues, ...issues], issue => issue.key),
@@ -534,6 +530,16 @@ export default class SourceViewerBase extends React.Component {
}));
};
+ handleIssueChange = (issue: Issue) => {
+ this.setState(state => {
+ const issues = state.issues.map(candidate => candidate.key === issue.key ? issue : candidate);
+ return { issues, issuesByLine: issuesByLine(issues) };
+ });
+ if (this.props.onIssueChange) {
+ this.props.onIssueChange(issue);
+ }
+ };
+
renderCode(sources: Array<SourceLine>) {
const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
return (
@@ -561,6 +567,7 @@ export default class SourceViewerBase extends React.Component {
loadingSourcesBefore={this.state.loadingSourcesBefore}
onCoverageClick={this.handleCoverageClick}
onDuplicationClick={this.handleDuplicationClick}
+ onIssueChange={this.handleIssueChange}
onIssueSelect={this.handleIssueSelect}
onIssueUnselect={this.handleIssueUnselect}
onIssuesOpen={this.handleOpenIssues}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
index 8b9cfb46bd5..64aeedd5ba6 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
@@ -40,7 +40,7 @@ const ZERO_LINE = {
};
export default class SourceViewerCode extends React.PureComponent {
- props: {
+ props: {|
displayAllIssues: boolean,
duplications?: Array<Duplication>,
duplicationsByLine: { [number]: Array<number> },
@@ -51,7 +51,7 @@ export default class SourceViewerCode extends React.PureComponent {
highlightedLine: number | null,
highlightedSymbols: Array<string>,
issues: Array<Issue>,
- issuesByLine: { [number]: Array<string> },
+ issuesByLine: { [number]: Array<Issue> },
issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
@@ -62,6 +62,7 @@ export default class SourceViewerCode extends React.PureComponent {
loadingSourcesBefore: boolean,
onCoverageClick: (SourceLine, HTMLElement) => void,
onDuplicationClick: (number, number) => void,
+ onIssueChange: (Issue) => void,
onIssueSelect: (string) => void,
onIssueUnselect: () => void,
onIssuesOpen: (SourceLine) => void,
@@ -75,13 +76,13 @@ export default class SourceViewerCode extends React.PureComponent {
selectedIssueLocation: IndexedIssueLocation | null,
sources: Array<SourceLine>,
symbolsByLine: { [number]: Array<string> }
- };
+ |};
getDuplicationsForLine(line: SourceLine) {
return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
}
- getIssuesForLine(line: SourceLine): Array<string> {
+ getIssuesForLine(line: SourceLine): Array<Issue> {
return this.props.issuesByLine[line.line] || EMPTY_ARRAY;
}
@@ -98,8 +99,11 @@ export default class SourceViewerCode extends React.PureComponent {
}
getSecondaryIssueLocationMessagesForLine(line: SourceLine, issueKey: string) {
- return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] ||
- EMPTY_ARRAY;
+ const index = this.props.issueSecondaryLocationMessagesByIssueByLine;
+ if (index[issueKey] == null) {
+ return EMPTY_ARRAY;
+ }
+ return index[issueKey][line.line] || EMPTY_ARRAY;
}
renderLine = (
@@ -131,7 +135,8 @@ export default class SourceViewerCode extends React.PureComponent {
optimizedHighlightedSymbols = EMPTY_ARRAY;
}
- const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue)
+ const optimizedSelectedIssue = selectedIssue != null &&
+ issuesForLine.find(issue => issue.key === selectedIssue)
? selectedIssue
: null;
@@ -165,6 +170,7 @@ export default class SourceViewerCode extends React.PureComponent {
onClick={this.props.onLineClick}
onCoverageClick={this.props.onCoverageClick}
onDuplicationClick={this.props.onDuplicationClick}
+ onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onIssueUnselect={this.props.onIssueUnselect}
onIssuesOpen={this.props.onIssuesOpen}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
index 4b65cd32ede..523ceeb192f 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import { Link } from 'react-router';
-import QualifierIcon from '../shared/qualifier-icon';
+import QualifierIcon from '../shared/QualifierIcon';
import FavoriteContainer from '../controls/FavoriteContainer';
import { getProjectUrl, getIssuesUrl } from '../../helpers/urls';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
@@ -44,7 +44,8 @@ export default class SourceViewerHeader extends React.PureComponent {
projectName: string,
q: string,
subProject?: string,
- subProjectName?: string
+ subProjectName?: string,
+ uuid: string
},
openNewWindow: () => void,
showMeasures: () => void
@@ -76,7 +77,8 @@ export default class SourceViewerHeader extends React.PureComponent {
projectName,
q,
subProject,
- subProjectName
+ subProjectName,
+ uuid
} = this.props.component;
const isUnitTest = q === 'UTS';
// TODO check if source viewer is displayed inside workspace
@@ -169,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent {
<div className="source-viewer-header-measure">
<span className="source-viewer-header-measure-value">
<Link
- to={getIssuesUrl({ resolved: 'false', componentKeys: key })}
+ to={getIssuesUrl({ resolved: 'false', fileUuids: uuid })}
className="source-viewer-header-external-link"
target="_blank">
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
index 74f3dabbfae..b1d051389a5 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
@@ -26,7 +26,7 @@ import LineSCM from './LineSCM';
import LineCoverage from './LineCoverage';
import LineDuplications from './LineDuplications';
import LineDuplicationBlock from './LineDuplicationBlock';
-import LineIssuesIndicatorContainer from './LineIssuesIndicatorContainer';
+import LineIssuesIndicator from './LineIssuesIndicator';
import LineCode from './LineCode';
import { TooltipsContainer } from '../../mixins/tooltips-mixin';
import type { SourceLine } from '../types';
@@ -35,8 +35,9 @@ import type {
IndexedIssueLocation,
IndexedIssueLocationMessage
} from '../helpers/indexing';
+import type { Issue } from '../../issue/types';
-type Props = {
+type Props = {|
displayAllIssues: boolean,
displayCoverage: boolean,
displayDuplications: boolean,
@@ -48,12 +49,13 @@ type Props = {
highlighted: boolean,
highlightedSymbols: Array<string>,
issueLocations: Array<LinearIssueLocation>,
- issues: Array<string>,
+ issues: Array<Issue>,
line: SourceLine,
loadDuplications: (SourceLine, HTMLElement) => void,
onClick: (SourceLine, HTMLElement) => void,
onCoverageClick: (SourceLine, HTMLElement) => void,
onDuplicationClick: (number, number) => void,
+ onIssueChange: (Issue) => void,
onIssueSelect: (string) => void,
onIssueUnselect: () => void,
onIssuesOpen: (SourceLine) => void,
@@ -68,7 +70,7 @@ type Props = {
// $FlowFixMe
secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
selectedIssueLocation: IndexedIssueLocation | null
-};
+|};
export default class Line extends React.PureComponent {
props: Props;
@@ -82,7 +84,7 @@ export default class Line extends React.PureComponent {
const { issues } = this.props;
if (issues.length > 0) {
- this.props.onIssueSelect(issues[0]);
+ this.props.onIssueSelect(issues[0].key);
}
}
};
@@ -124,8 +126,8 @@ export default class Line extends React.PureComponent {
{this.props.displayIssues &&
!this.props.displayAllIssues &&
- <LineIssuesIndicatorContainer
- issueKeys={this.props.issues}
+ <LineIssuesIndicator
+ issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>}
@@ -137,9 +139,10 @@ export default class Line extends React.PureComponent {
<LineCode
highlightedSymbols={this.props.highlightedSymbols}
- issueKeys={this.props.issues}
+ issues={this.props.issues}
issueLocations={this.props.issueLocations}
line={line}
+ onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
index b5813f76365..6b18e06791b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
@@ -34,12 +34,14 @@ import type {
IndexedIssueLocation,
IndexedIssueLocationMessage
} from '../helpers/indexing';
+import type { Issue } from '../../issue/types';
-type Props = {
+type Props = {|
highlightedSymbols: Array<string>,
- issueKeys: Array<string>,
+ issues: Array<Issue>,
issueLocations: Array<LinearIssueLocation>,
line: SourceLine,
+ onIssueChange: (Issue) => void,
onIssueSelect: (issueKey: string) => void,
onLocationSelect: (flowIndex: number, locationIndex: number) => void,
onSymbolClick: (Array<string>) => void,
@@ -49,7 +51,7 @@ type Props = {
selectedIssue: string | null,
selectedIssueLocation: IndexedIssueLocation | null,
showIssues: boolean
-};
+|};
type State = {
tokens: Tokens
@@ -166,7 +168,7 @@ export default class LineCode extends React.PureComponent {
render() {
const {
highlightedSymbols,
- issueKeys,
+ issues,
issueLocations,
line,
onIssueSelect,
@@ -201,7 +203,7 @@ export default class LineCode extends React.PureComponent {
const finalCode = generateHTML(tokens);
const className = classNames('source-line-code', 'code', {
- 'has-issues': issueKeys.length > 0
+ 'has-issues': issues.length > 0
});
return (
@@ -213,9 +215,10 @@ export default class LineCode extends React.PureComponent {
this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)}
</div>
{showIssues &&
- issueKeys.length > 0 &&
+ issues.length > 0 &&
<LineIssuesList
- issueKeys={issueKeys}
+ issues={issues}
+ onIssueChange={this.props.onIssueChange}
onIssueClick={onIssueSelect}
selectedIssue={selectedIssue}
/>}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
index daf1785ffd2..b7f1c2a176e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
@@ -23,9 +23,10 @@ import classNames from 'classnames';
import SeverityIcon from '../../shared/SeverityIcon';
import { sortBySeverity } from '../../../helpers/issues';
import type { SourceLine } from '../types';
+import type { Issue } from '../../issue/types';
type Props = {
- issues: Array<{ severity: string }>,
+ issues: Array<Issue>,
line: SourceLine,
onClick: () => void
};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
index ca89ab51fae..bff245af97c 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
@@ -19,10 +19,12 @@
*/
// @flow
import React from 'react';
-import ConnectedIssue from '../../issue/ConnectedIssue';
+import Issue from '../../issue/Issue';
+import type { Issue as IssueType } from '../../issue/types';
type Props = {
- issueKeys: Array<string>,
+ issues: Array<IssueType>,
+ onIssueChange: (IssueType) => void,
onIssueClick: (issueKey: string) => void,
selectedIssue: string | null
};
@@ -31,16 +33,17 @@ export default class LineIssuesList extends React.PureComponent {
props: Props;
render() {
- const { issueKeys, onIssueClick, selectedIssue } = this.props;
+ const { issues, onIssueClick, selectedIssue } = this.props;
return (
<div className="issue-list">
- {issueKeys.map(issueKey => (
- <ConnectedIssue
- issueKey={issueKey}
- key={issueKey}
+ {issues.map(issue => (
+ <Issue
+ issue={issue}
+ key={issue.key}
+ onChange={this.props.onIssueChange}
onClick={onIssueClick}
- selected={selectedIssue === issueKey}
+ selected={selectedIssue === issue.key}
/>
))}
</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
index 3cc6793b214..5cd841a2a5a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
@@ -34,7 +34,7 @@ it('render code', () => {
const wrapper = shallow(
<LineCode
highlightedSymbols={['sym1']}
- issueKeys={['issue-1', 'issue-2']}
+ issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
issueLocations={issueLocations}
line={line}
onIssueSelect={jest.fn()}
@@ -62,7 +62,7 @@ it('should handle empty location message', () => {
const wrapper = shallow(
<LineCode
highlightedSymbols={['sym1']}
- issueKeys={['issue-1', 'issue-2']}
+ issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
issueLocations={issueLocations}
line={line}
onIssueSelect={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
index ede9d50241c..a9a0e0763b9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
@@ -23,15 +23,10 @@ import LineIssuesList from '../LineIssuesList';
it('render issues list', () => {
const line = { line: 3 };
- const issueKeys = ['foo', 'bar'];
+ const issues = [{ key: 'foo' }, { key: 'bar' }];
const onIssueClick = jest.fn();
const wrapper = shallow(
- <LineIssuesList
- issueKeys={issueKeys}
- line={line}
- onIssueClick={onIssueClick}
- selectedIssue="foo"
- />
+ <LineIssuesList issues={issues} line={line} onIssueClick={onIssueClick} selectedIssue="foo" />
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
index 3e4499bb1bf..ca94ecedbf7 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
@@ -22,10 +22,14 @@ exports[`test render code 1`] = `
</div>
</div>
<LineIssuesList
- issueKeys={
+ issues={
Array [
- "issue-1",
- "issue-2",
+ Object {
+ "key": "issue-1",
+ },
+ Object {
+ "key": "issue-2",
+ },
]
}
onIssueClick={[Function]}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
index 30bbfaa7779..090d0852df8 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
@@ -1,12 +1,20 @@
exports[`test render issues list 1`] = `
<div
className="issue-list">
- <Connect(BaseIssue)
- issueKey="foo"
+ <BaseIssue
+ issue={
+ Object {
+ "key": "foo",
+ }
+ }
onClick={[Function]}
selected={true} />
- <Connect(BaseIssue)
- issueKey="bar"
+ <BaseIssue
+ issue={
+ Object {
+ "key": "bar",
+ }
+ }
onClick={[Function]}
selected={false} />
</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
index 36bf7e73b3a..d39351c52dc 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
@@ -64,7 +64,7 @@ export const issuesByLine = (issues: Array<Issue>) => {
if (!(line in index)) {
index[line] = [];
}
- index[line].push(issue.key);
+ index[line].push(issue);
});
return index;
};
diff --git a/server/sonar-web/src/main/js/components/__tests__/issue-test.js b/server/sonar-web/src/main/js/components/__tests__/issue-test.js
deleted file mode 100644
index b0483b8cc21..00000000000
--- a/server/sonar-web/src/main/js/components/__tests__/issue-test.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Issue from '../issue/models/issue';
-
-describe('Model', () => {
- it('should have correct urlRoot', () => {
- const issue = new Issue();
- expect(issue.urlRoot()).toBe('/api/issues');
- });
-
- it('should parse response without root issue object', () => {
- const issue = new Issue();
- const example = { a: 1 };
- expect(issue.parse(example)).toEqual(example);
- });
-
- it('should parse response with the root issue object', () => {
- const issue = new Issue();
- const example = { a: 1 };
- expect(issue.parse({ issue: example })).toEqual(example);
- });
-
- it('should reset attributes (no attributes initially)', () => {
- const issue = new Issue();
- const example = { a: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should reset attributes (override attribute)', () => {
- const issue = new Issue({ a: 2 });
- const example = { a: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should reset attributes (different attributes)', () => {
- const issue = new Issue({ a: 2 });
- const example = { b: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should unset `textRange` of a closed issue', () => {
- const issue = new Issue();
- const result = issue.parse({ issue: { status: 'CLOSED', textRange: { startLine: 5 } } });
- expect(result.textRange).toBeFalsy();
- });
-
- it('should unset `flows` of a closed issue', () => {
- const issue = new Issue();
- const result = issue.parse({ issue: { status: 'CLOSED', flows: [1, 2, 3] } });
- expect(result.flows).toEqual([]);
- });
-
- describe('Actions', () => {
- it('should assign', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.assign('admin');
- expect(spy).toBeCalledWith({
- data: { assignee: 'admin', issue: 'issue-key' },
- url: '/api/issues/assign'
- });
- });
-
- it('should unassign', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.assign();
- expect(spy).toBeCalledWith({
- data: { assignee: undefined, issue: 'issue-key' },
- url: '/api/issues/assign'
- });
- });
-
- it('should plan', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.plan('plan');
- expect(spy).toBeCalledWith({
- data: { plan: 'plan', issue: 'issue-key' },
- url: '/api/issues/plan'
- });
- });
-
- it('should unplan', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.plan();
- expect(spy).toBeCalledWith({
- data: { plan: undefined, issue: 'issue-key' },
- url: '/api/issues/plan'
- });
- });
-
- it('should set severity', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.setSeverity('BLOCKER');
- expect(spy).toBeCalledWith({
- data: { severity: 'BLOCKER', issue: 'issue-key' },
- url: '/api/issues/set_severity'
- });
- });
- });
-
- describe('#getLinearLocations', () => {
- it('should return single line location', () => {
- const issue = new Issue({
- textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(1);
-
- expect(locations[0].line).toBe(1);
- expect(locations[0].from).toBe(0);
- expect(locations[0].to).toBe(10);
- });
-
- it('should return location not from 0', () => {
- const issue = new Issue({
- textRange: { startLine: 1, endLine: 1, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(1);
-
- expect(locations[0].line).toBe(1);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(10);
- });
-
- it('should return 2-lines location', () => {
- const issue = new Issue({
- textRange: { startLine: 2, endLine: 3, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(2);
-
- expect(locations[0].line).toBe(2);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(999999);
-
- expect(locations[1].line).toBe(3);
- expect(locations[1].from).toBe(0);
- expect(locations[1].to).toBe(10);
- });
-
- it('should return 3-lines location', () => {
- const issue = new Issue({
- textRange: { startLine: 4, endLine: 6, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(3);
-
- expect(locations[0].line).toBe(4);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(999999);
-
- expect(locations[1].line).toBe(5);
- expect(locations[1].from).toBe(0);
- expect(locations[1].to).toBe(999999);
-
- expect(locations[2].line).toBe(6);
- expect(locations[2].from).toBe(0);
- expect(locations[2].to).toBe(10);
- });
-
- it('should return [] when no location', () => {
- const issue = new Issue();
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(0);
- });
- });
-});
diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js
index dc4f0082f27..4e1d336dc0d 100644
--- a/server/sonar-web/src/main/js/components/charts/bar-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js
@@ -21,7 +21,7 @@ import React from 'react';
import { max } from 'd3-array';
import { scaleLinear, scaleBand } from 'd3-scale';
import { ResizeMixin } from './../mixins/resize-mixin';
-import { TooltipsMixin } from './../mixins/tooltips-mixin';
+import { TooltipsContainer } from './../mixins/tooltips-mixin';
export const BarChart = React.createClass({
propTypes: {
@@ -34,7 +34,7 @@ export const BarChart = React.createClass({
onBarClick: React.PropTypes.func
},
- mixins: [ResizeMixin, TooltipsMixin],
+ mixins: [ResizeMixin],
getDefaultProps() {
return {
@@ -162,13 +162,15 @@ export const BarChart = React.createClass({
const yScale = scaleLinear().domain([0, maxY]).range([availableHeight, 0]);
return (
- <svg className="bar-chart" width={this.state.width} height={this.state.height}>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderXTicks(xScale, yScale)}
- {this.renderXValues(xScale, yScale)}
- {this.renderBars(xScale, yScale)}
- </g>
- </svg>
+ <TooltipsContainer>
+ <svg className="bar-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderXTicks(xScale, yScale)}
+ {this.renderXValues(xScale, yScale)}
+ {this.renderBars(xScale, yScale)}
+ </g>
+ </svg>
+ </TooltipsContainer>
);
}
});
diff --git a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
index d91207af777..7b06317b39e 100644
--- a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
+++ b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import QualifierIcon from '../shared/qualifier-icon';
+import QualifierIcon from '../shared/QualifierIcon';
export const TreemapBreadcrumbs = React.createClass({
propTypes: {
diff --git a/server/sonar-web/src/main/js/components/common/EmptySearch.js b/server/sonar-web/src/main/js/components/common/EmptySearch.js
new file mode 100644
index 00000000000..904a6b2cbad
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/EmptySearch.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+import { translate } from '../../helpers/l10n';
+
+const EmptySearch = () => (
+ <div
+ className={css({
+ padding: '60px 0',
+ border: '1px solid #e6e6e6',
+ borderRadius: 2,
+ textAlign: 'center',
+ color: '#777'
+ })}>
+ <h3>{translate('no_results_search')}</h3>
+ <p className="big-spacer-top">{translate('no_results_search.2')}</p>
+ </div>
+);
+
+export default EmptySearch;
diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
index 2d83b6aeb24..8c5db3a8fe1 100644
--- a/server/sonar-web/src/main/js/components/common/MarkdownTips.js
+++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
@@ -25,7 +25,7 @@ import { translate } from '../../helpers/l10n';
export default class MarkdownTips extends React.PureComponent {
handleClick(evt: MouseEvent) {
evt.preventDefault();
- window.open(getMarkdownHelpUrl(), 'height=300,width=600,scrollbars=1,resizable=1');
+ window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1');
}
render() {
diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js
index ba2f82b34b7..bec5c2e6712 100644
--- a/server/sonar-web/src/main/js/components/common/SelectList.js
+++ b/server/sonar-web/src/main/js/components/common/SelectList.js
@@ -19,6 +19,8 @@
*/
// @flow
import React from 'react';
+import key from 'keymaster';
+import { uniqueId } from 'lodash';
import SelectListItem from './SelectListItem';
type Props = {
@@ -33,7 +35,8 @@ type State = {
};
export default class SelectList extends React.PureComponent {
- list: HTMLElement;
+ currentKeyScope: string;
+ previousKeyScope: string;
props: Props;
state: State;
@@ -45,7 +48,7 @@ export default class SelectList extends React.PureComponent {
}
componentDidMount() {
- this.list.focus();
+ this.attachShortcuts();
}
componentWillReceiveProps(nextProps: Props) {
@@ -57,24 +60,36 @@ export default class SelectList extends React.PureComponent {
}
}
- handleKeyboard = (evt: KeyboardEvent) => {
- switch (evt.keyCode) {
- case 40: // down
- this.setState(this.selectNextElement);
- break;
- case 38: // up
- this.setState(this.selectPreviousElement);
- break;
- case 13: // return
- if (this.state.active) {
- this.handleSelect(this.state.active);
- }
- break;
- default:
- return;
- }
- evt.preventDefault();
- evt.stopPropagation();
+ componentWillUnmount() {
+ this.detachShortcuts();
+ }
+
+ attachShortcuts = () => {
+ this.previousKeyScope = key.getScope();
+ this.currentKeyScope = uniqueId('key-scope');
+ key.setScope(this.currentKeyScope);
+
+ key('down', this.currentKeyScope, () => {
+ this.setState(this.selectNextElement);
+ return false;
+ });
+
+ key('up', this.currentKeyScope, () => {
+ this.setState(this.selectPreviousElement);
+ return false;
+ });
+
+ key('return', this.currentKeyScope, () => {
+ if (this.state.active) {
+ this.handleSelect(this.state.active);
+ }
+ return false;
+ });
+ };
+
+ detachShortcuts = () => {
+ key.setScope(this.previousKeyScope);
+ key.deleteScope(this.currentKeyScope);
};
handleSelect = (item: string) => {
@@ -105,18 +120,18 @@ export default class SelectList extends React.PureComponent {
const { children } = this.props;
const hasChildren = React.Children.count(children) > 0;
return (
- <ul
- className="menu"
- onKeyDown={this.handleKeyboard}
- ref={list => this.list = list}
- tabIndex={0}>
+ <ul className="menu">
{hasChildren &&
- React.Children.map(children, child =>
- React.cloneElement(child, {
- active: this.state.active,
- onHover: this.handleHover,
- onSelect: this.handleSelect
- }))}
+ React.Children.map(
+ children,
+ child =>
+ child != null &&
+ React.cloneElement(child, {
+ active: this.state.active,
+ onHover: this.handleHover,
+ onSelect: this.handleSelect
+ })
+ )}
{!hasChildren &&
this.props.items.map(item => (
<SelectListItem
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
index 9c0e88e6aa3..58afbecad26 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
+++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
@@ -64,11 +64,11 @@ it('should correclty handle user actions', () => {
))}
</SelectList>
);
- keydown(list.find('ul'), 40);
+ keydown(40);
expect(list.state()).toMatchSnapshot();
- keydown(list.find('ul'), 40);
+ keydown(40);
expect(list.state()).toMatchSnapshot();
- keydown(list.find('ul'), 38);
+ keydown(38);
expect(list.state()).toMatchSnapshot();
click(list.childAt(2).find('a'));
expect(onSelect.mock.calls).toMatchSnapshot(); // eslint-disable-linelist
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
index 4cf15f469cb..b2d9388c7ef 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
@@ -26,9 +26,7 @@ Array [
exports[`test should render correctly with children 1`] = `
<ul
- className="menu"
- onKeyDown={[Function]}
- tabIndex={0}>
+ className="menu">
<SelectListItem
active="seconditem"
item="item"
@@ -61,9 +59,7 @@ exports[`test should render correctly with children 1`] = `
exports[`test should render correctly without children 1`] = `
<ul
- className="menu"
- onKeyDown={[Function]}
- tabIndex={0}>
+ className="menu">
<SelectListItem
active="seconditem"
item="item"
diff --git a/server/sonar-web/src/main/js/components/common/action-options-view.js b/server/sonar-web/src/main/js/components/common/action-options-view.js
index 976f88eb081..a538e22b88e 100644
--- a/server/sonar-web/src/main/js/components/common/action-options-view.js
+++ b/server/sonar-web/src/main/js/components/common/action-options-view.js
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
+import key from 'keymaster';
import PopupView from './popup';
export default PopupView.extend({
diff --git a/server/sonar-web/src/main/js/components/common/modals.js b/server/sonar-web/src/main/js/components/common/modals.js
index 5a1343bd511..a86a262d40c 100644
--- a/server/sonar-web/src/main/js/components/common/modals.js
+++ b/server/sonar-web/src/main/js/components/common/modals.js
@@ -19,6 +19,7 @@
*/
import $ from 'jquery';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
const EVENT_SCOPE = 'modal';
diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js
index 532380f71b4..bce61b72938 100644
--- a/server/sonar-web/src/main/js/components/common/popup.js
+++ b/server/sonar-web/src/main/js/components/common/popup.js
@@ -19,6 +19,7 @@
*/
import $ from 'jquery';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
export default Marionette.ItemView.extend({
className: 'bubble-popup',
diff --git a/server/sonar-web/src/main/js/components/controls/Checkbox.js b/server/sonar-web/src/main/js/components/controls/Checkbox.js
index f5e7289dd45..c81d49b8d8c 100644
--- a/server/sonar-web/src/main/js/components/controls/Checkbox.js
+++ b/server/sonar-web/src/main/js/components/controls/Checkbox.js
@@ -26,7 +26,7 @@ export default class Checkbox extends React.Component {
onCheck: React.PropTypes.func.isRequired,
checked: React.PropTypes.bool.isRequired,
thirdState: React.PropTypes.bool,
- className: React.PropTypes.string
+ className: React.PropTypes.any
};
static defaultProps = {
@@ -44,7 +44,9 @@ export default class Checkbox extends React.Component {
}
render() {
- const className = classNames(this.props.className, 'icon-checkbox', {
+ const className = classNames('icon-checkbox', {
+ // trick to work with glamor
+ [this.props.className]: true,
'icon-checkbox-checked': this.props.checked,
'icon-checkbox-single': this.props.thirdState
});
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.js b/server/sonar-web/src/main/js/components/controls/DateInput.js
index 52b47cbe189..f4070c12119 100644
--- a/server/sonar-web/src/main/js/components/controls/DateInput.js
+++ b/server/sonar-web/src/main/js/components/controls/DateInput.js
@@ -19,11 +19,13 @@
*/
import $ from 'jquery';
import React from 'react';
+import classNames from 'classnames';
import { pick } from 'lodash';
import './styles.css';
export default class DateInput extends React.Component {
static propTypes = {
+ className: React.PropTypes.string,
value: React.PropTypes.string,
format: React.PropTypes.string,
name: React.PropTypes.string,
@@ -67,12 +69,12 @@ export default class DateInput extends React.Component {
/* eslint max-len: 0 */
return (
- <span className="date-input-control">
+ <span className={classNames('date-input-control', this.props.className)}>
<input
className="date-input-control-input"
ref="input"
type="text"
- initialValue={this.props.value}
+ defaultValue={this.props.value}
readOnly={true}
{...inputProps}
/>
diff --git a/server/sonar-web/src/main/js/components/issue/BaseIssue.js b/server/sonar-web/src/main/js/components/issue/BaseIssue.js
deleted file mode 100644
index d4ada02869b..00000000000
--- a/server/sonar-web/src/main/js/components/issue/BaseIssue.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import IssueView from './IssueView';
-import { setIssueAssignee } from '../../api/issues';
-import type { Issue } from './types';
-
-type Props = {
- checked?: boolean,
- issue: Issue,
- onCheck?: () => void,
- onClick: (string) => void,
- onFail: (Error) => void,
- onFilterClick?: () => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
- selected: boolean
-};
-
-type State = {
- currentPopup: string
-};
-
-export default class BaseIssue extends React.PureComponent {
- mounted: boolean;
- props: Props;
- state: State;
-
- static defaultProps = {
- selected: false
- };
-
- constructor(props: Props) {
- super(props);
- this.state = {
- currentPopup: ''
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- if (this.props.selected) {
- this.bindShortcuts();
- }
- }
-
- componentWillUpdate(nextProps: Props) {
- if (!nextProps.selected && this.props.selected) {
- this.unbindShortcuts();
- }
- }
-
- componentDidUpdate(prevProps: Props) {
- if (!prevProps.selected && this.props.selected) {
- this.bindShortcuts();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- if (this.props.selected) {
- this.unbindShortcuts();
- }
- }
-
- bindShortcuts() {
- document.addEventListener('keypress', this.handleKeyPress);
- }
-
- unbindShortcuts() {
- document.removeEventListener('keypress', this.handleKeyPress);
- }
-
- togglePopup = (popupName: string, open?: boolean) => {
- if (this.mounted) {
- this.setState((prevState: State) => {
- if (prevState.currentPopup !== popupName && open !== false) {
- return { currentPopup: popupName };
- } else if (prevState.currentPopup === popupName && open !== true) {
- return { currentPopup: '' };
- }
- return prevState;
- });
- }
- };
-
- handleAssignement = (login: string) => {
- const { issue } = this.props;
- if (issue.assignee !== login) {
- this.props.onIssueChange(setIssueAssignee({ issue: issue.key, assignee: login }));
- }
- this.togglePopup('assign', false);
- };
-
- handleKeyPress = (e: Object) => {
- const tagName = e.target.tagName.toUpperCase();
- const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';
-
- if (shouldHandle) {
- switch (e.key) {
- case 'f':
- return this.togglePopup('transition');
- case 'a':
- return this.togglePopup('assign');
- case 'm':
- return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
- case 'p':
- return this.togglePopup('plan');
- case 'i':
- return this.togglePopup('set-severity');
- case 'c':
- return this.togglePopup('comment');
- case 't':
- return this.togglePopup('edit-tags');
- }
- }
- };
-
- render() {
- return (
- <IssueView
- issue={this.props.issue}
- checked={this.props.checked}
- onAssign={this.handleAssignement}
- onCheck={this.props.onCheck}
- onClick={this.props.onClick}
- onFail={this.props.onFail}
- onFilterClick={this.props.onFilterClick}
- onIssueChange={this.props.onIssueChange}
- togglePopup={this.togglePopup}
- currentPopup={this.state.currentPopup}
- selected={this.props.selected}
- />
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js
index a121bf738d0..471a62e04f0 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.js
+++ b/server/sonar-web/src/main/js/components/issue/Issue.js
@@ -18,14 +18,145 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import { connect } from 'react-redux';
-import BaseIssue from './BaseIssue';
-import { onFail } from '../../store/rootActions';
+import React from 'react';
+import IssueView from './IssueView';
import { updateIssue } from './actions';
+import { setIssueAssignee } from '../../api/issues';
+import { onFail } from '../../store/rootActions';
+import type { Issue } from './types';
+
+type Props = {|
+ checked?: boolean,
+ issue: Issue,
+ onChange: (Issue) => void,
+ onCheck?: (string) => void,
+ onClick: (string) => void,
+ onFilter?: (property: string, issue: Issue) => void,
+ selected: boolean
+|};
-const mapDispatchToProps = {
- onIssueChange: updateIssue,
- onFail: error => dispatch => onFail(dispatch)(error)
+type State = {
+ currentPopup: string
};
-export default connect(null, mapDispatchToProps)(BaseIssue);
+export default class BaseIssue extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ static contextTypes = {
+ store: React.PropTypes.object
+ };
+
+ static defaultProps = {
+ selected: false
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ currentPopup: ''
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.selected) {
+ this.bindShortcuts();
+ }
+ }
+
+ componentWillUpdate(nextProps: Props) {
+ if (!nextProps.selected && this.props.selected) {
+ this.unbindShortcuts();
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!prevProps.selected && this.props.selected) {
+ this.bindShortcuts();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ if (this.props.selected) {
+ this.unbindShortcuts();
+ }
+ }
+
+ bindShortcuts() {
+ document.addEventListener('keypress', this.handleKeyPress);
+ }
+
+ unbindShortcuts() {
+ document.removeEventListener('keypress', this.handleKeyPress);
+ }
+
+ togglePopup = (popupName: string, open?: boolean) => {
+ if (this.mounted) {
+ this.setState((prevState: State) => {
+ if (prevState.currentPopup !== popupName && open !== false) {
+ return { currentPopup: popupName };
+ } else if (prevState.currentPopup === popupName && open !== true) {
+ return { currentPopup: '' };
+ }
+ return prevState;
+ });
+ }
+ };
+
+ handleAssignement = (login: string) => {
+ const { issue } = this.props;
+ if (issue.assignee !== login) {
+ updateIssue(this.props.onChange, setIssueAssignee({ issue: issue.key, assignee: login }));
+ }
+ this.togglePopup('assign', false);
+ };
+
+ handleFail = (error: Error) => {
+ onFail(this.context.store.dispatch)(error);
+ };
+
+ handleKeyPress = (e: Object) => {
+ const tagName = e.target.tagName.toUpperCase();
+ const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';
+
+ if (shouldHandle) {
+ switch (e.key) {
+ case 'f':
+ return this.togglePopup('transition');
+ case 'a':
+ return this.togglePopup('assign');
+ case 'm':
+ return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
+ case 'p':
+ return this.togglePopup('plan');
+ case 'i':
+ return this.togglePopup('set-severity');
+ case 'c':
+ return this.togglePopup('comment');
+ case 't':
+ return this.togglePopup('edit-tags');
+ }
+ }
+ };
+
+ render() {
+ return (
+ <IssueView
+ issue={this.props.issue}
+ checked={this.props.checked}
+ onAssign={this.handleAssignement}
+ onCheck={this.props.onCheck}
+ onClick={this.props.onClick}
+ onFail={this.handleFail}
+ onFilter={this.props.onFilter}
+ onChange={this.props.onChange}
+ togglePopup={this.togglePopup}
+ currentPopup={this.state.currentPopup}
+ selected={this.props.selected}
+ />
+ );
+ }
+}
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 52ee7e95280..3d959f26573 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueView.js
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.js
@@ -20,43 +20,51 @@
// @flow
import React from 'react';
import classNames from 'classnames';
-import Checkbox from '../../components/controls/Checkbox';
import IssueTitleBar from './components/IssueTitleBar';
import IssueActionsBar from './components/IssueActionsBar';
import IssueCommentLine from './components/IssueCommentLine';
+import { updateIssue } from './actions';
import { deleteIssueComment, editIssueComment } from '../../api/issues';
import type { Issue } from './types';
-type Props = {
+type Props = {|
checked?: boolean,
currentPopup: string,
issue: Issue,
onAssign: (string) => void,
- onCheck?: () => void,
+ onChange: (Issue) => void,
+ onCheck?: (string) => void,
onClick: (string) => void,
onFail: (Error) => void,
- onFilterClick?: () => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
+ onFilter?: (property: string, issue: Issue) => void,
selected: boolean,
togglePopup: (string) => void
-};
+|};
export default class IssueView extends React.PureComponent {
props: Props;
- handleClick = (evt: MouseEvent) => {
- evt.preventDefault();
+ handleCheck = (event: Event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (this.props.onCheck) {
+ this.props.onCheck(this.props.issue.key);
+ }
+ };
+
+ handleClick = (event: Event & { target: HTMLElement }) => {
+ event.preventDefault();
if (this.props.onClick) {
this.props.onClick(this.props.issue.key);
}
};
editComment = (comment: string, text: string) => {
- this.props.onIssueChange(editIssueComment({ comment, text }));
+ updateIssue(this.props.onChange, editIssueComment({ comment, text }));
};
deleteComment = (comment: string) => {
- this.props.onIssueChange(deleteIssueComment({ comment }));
+ updateIssue(this.props.onChange, deleteIssueComment({ comment }));
};
render() {
@@ -74,13 +82,13 @@ export default class IssueView extends React.PureComponent {
className={issueClass}
data-issue={issue.key}
onClick={this.handleClick}
- tabIndex={0}
- role="listitem">
+ role="listitem"
+ tabIndex={0}>
<IssueTitleBar
issue={issue}
currentPopup={this.props.currentPopup}
onFail={this.props.onFail}
- onFilterClick={this.props.onFilterClick}
+ onFilter={this.props.onFilter}
togglePopup={this.props.togglePopup}
/>
<IssueActionsBar
@@ -89,7 +97,7 @@ export default class IssueView extends React.PureComponent {
onAssign={this.props.onAssign}
onFail={this.props.onFail}
togglePopup={this.props.togglePopup}
- onIssueChange={this.props.onIssueChange}
+ onChange={this.props.onChange}
/>
{issue.comments &&
issue.comments.length > 0 &&
@@ -108,13 +116,13 @@ export default class IssueView extends React.PureComponent {
<i className="issue-navigate-to-right icon-chevron-right" />
</a>
{hasCheckbox &&
- <div className="js-toggle issue-checkbox-container">
- <Checkbox
- className="issue-checkbox"
- onCheck={this.props.onCheck}
- checked={this.props.checked}
+ <a className="js-toggle issue-checkbox-container" href="#" onClick={this.handleCheck}>
+ <i
+ className={classNames('issue-checkbox', 'icon-checkbox', {
+ 'icon-checkbox-checked': this.props.checked
+ })}
/>
- </div>}
+ </a>}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/actions.js b/server/sonar-web/src/main/js/components/issue/actions.js
index a0631c17001..a44430520bd 100644
--- a/server/sonar-web/src/main/js/components/issue/actions.js
+++ b/server/sonar-web/src/main/js/components/issue/actions.js
@@ -18,35 +18,41 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import type { Dispatch } from 'redux';
-import type { Issue } from './types';
import { onFail } from '../../store/rootActions';
-import { receiveIssues } from '../../store/issues/duck';
import { parseIssueFromResponse } from '../../helpers/issues';
+import type { Issue } from './types';
-export const updateIssue = (resultPromise: Promise<*>, oldIssue?: Issue, newIssue?: Issue) =>
- (dispatch: Dispatch<*>) => {
- if (oldIssue && newIssue) {
- dispatch(receiveIssues([newIssue]));
- }
- resultPromise.then(
- response => {
- dispatch(
- receiveIssues([
- parseIssueFromResponse(
- response.issue,
- response.components,
- response.users,
- response.rules
- )
- ])
+export const updateIssue = (
+ onChange: (Issue) => void,
+ resultPromise: Promise<*>,
+ oldIssue?: Issue,
+ newIssue?: Issue
+) => {
+ const optimisticUpdate = oldIssue != null && newIssue != null;
+
+ if (optimisticUpdate) {
+ // $FlowFixMe `newIssue` is not null, because `optimisticUpdate` is true
+ onChange(newIssue);
+ }
+
+ resultPromise.then(
+ response => {
+ if (!optimisticUpdate) {
+ const issue = parseIssueFromResponse(
+ response.issue,
+ response.components,
+ response.users,
+ response.rules
);
- },
- error => {
- onFail(dispatch)(error);
- if (oldIssue && newIssue) {
- dispatch(receiveIssues([oldIssue]));
- }
+ onChange(issue);
+ }
+ },
+ error => {
+ onFail(error);
+ if (optimisticUpdate) {
+ // $FlowFixMe `oldIssue` is not null, because `optimisticUpdate` is true
+ onChange(oldIssue);
}
- );
- };
+ }
+ );
+};
diff --git a/server/sonar-web/src/main/js/components/issue/collections/issues.js b/server/sonar-web/src/main/js/components/issue/collections/issues.js
deleted file mode 100644
index 69ac37b1beb..00000000000
--- a/server/sonar-web/src/main/js/components/issue/collections/issues.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Backbone from 'backbone';
-import Issue from '../models/issue';
-
-export default Backbone.Collection.extend({
- model: Issue,
-
- url() {
- return window.baseUrl + '/api/issues/search';
- },
-
- _injectRelational(issue, source, baseField, lookupField) {
- const baseValue = issue[baseField];
- if (baseValue != null && Array.isArray(source) && source.length > 0) {
- const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
- if (lookupValue != null) {
- Object.keys(lookupValue).forEach(key => {
- const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
- issue[newKey] = lookupValue[key];
- });
- }
- }
- return issue;
- },
-
- _injectCommentsRelational(issue, users) {
- if (issue.comments) {
- const that = this;
- const newComments = issue.comments.map(comment => {
- let newComment = { ...comment, author: comment.login };
- delete newComment.login;
- newComment = that._injectRelational(newComment, users, 'author', 'login');
- return newComment;
- });
- issue = { ...issue, comments: newComments };
- }
- return issue;
- },
-
- _prepareClosed(issue) {
- if (issue.status === 'CLOSED') {
- issue.flows = [];
- delete issue.textRange;
- }
- return issue;
- },
-
- ensureTextRange(issue) {
- if (issue.line && !issue.textRange) {
- // FIXME 999999
- issue.textRange = {
- startLine: issue.line,
- endLine: issue.line,
- startOffset: 0,
- endOffset: 999999
- };
- }
- return issue;
- },
-
- parse(r) {
- const that = this;
-
- this.paging = {
- p: r.p,
- ps: r.ps,
- total: r.total,
- maxResultsReached: r.p * r.ps >= r.total
- };
-
- return r.issues.map(issue => {
- issue = that._injectRelational(issue, r.components, 'component', 'key');
- issue = that._injectRelational(issue, r.components, 'project', 'key');
- issue = that._injectRelational(issue, r.components, 'subProject', 'key');
- issue = that._injectRelational(issue, r.rules, 'rule', 'key');
- issue = that._injectRelational(issue, r.users, 'assignee', 'login');
- issue = that._injectCommentsRelational(issue, r.users);
- issue = that._prepareClosed(issue);
- issue = that.ensureTextRange(issue);
- return issue;
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
index e60bc87c991..9006cd9a19e 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
@@ -25,6 +25,7 @@ import IssueSeverity from './IssueSeverity';
import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import IssueType from './IssueType';
+import { updateIssue } from '../actions';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import type { Issue } from '../types';
@@ -32,8 +33,8 @@ type Props = {
issue: Issue,
currentPopup: string,
onAssign: (string) => void,
+ onChange: (Issue) => void,
onFail: (Error) => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
};
@@ -63,15 +64,18 @@ export default class IssueActionsBar extends React.PureComponent {
const { issue } = this.props;
if (issue[property] !== value) {
const newIssue = { ...issue, [property]: value };
- this.props.onIssueChange(apiCall({ issue: issue.key, [property]: value }), issue, newIssue);
+ updateIssue(
+ this.props.onChange,
+ apiCall({ issue: issue.key, [property]: value }),
+ issue,
+ newIssue
+ );
}
this.props.togglePopup(popup, false);
};
toggleComment = (open?: boolean, placeholder?: string) => {
- this.setState({
- commentPlaceholder: placeholder || ''
- });
+ this.setState({ commentPlaceholder: placeholder || '' });
this.props.togglePopup('comment', open);
};
@@ -112,8 +116,8 @@ export default class IssueActionsBar extends React.PureComponent {
isOpen={this.props.currentPopup === 'transition' && hasTransitions}
issue={issue}
hasTransitions={hasTransitions}
+ onChange={this.props.onChange}
togglePopup={this.props.togglePopup}
- setIssueProperty={this.setIssueProperty}
/>
</li>
<li className="issue-meta">
@@ -134,10 +138,10 @@ export default class IssueActionsBar extends React.PureComponent {
</li>}
{canComment &&
<IssueCommentAction
- issueKey={issue.key}
commentPlaceholder={this.state.commentPlaceholder}
currentPopup={this.props.currentPopup}
- onIssueChange={this.props.onIssueChange}
+ issueKey={issue.key}
+ onChange={this.props.onChange}
toggleComment={this.toggleComment}
/>}
</ul>
@@ -149,8 +153,8 @@ export default class IssueActionsBar extends React.PureComponent {
isOpen={this.props.currentPopup === 'edit-tags' && canSetTags}
canSetTags={canSetTags}
issue={issue}
+ onChange={this.props.onChange}
onFail={this.props.onFail}
- onIssueChange={this.props.onIssueChange}
togglePopup={this.props.togglePopup}
/>
</li>
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 f5f6bf5b8d3..8111815e942 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
@@ -19,25 +19,26 @@
*/
// @flow
import React from 'react';
+import { updateIssue } from '../actions';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import CommentPopup from '../popups/CommentPopup';
import { addIssueComment } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';
-type Props = {
- issueKey: string,
+type Props = {|
commentPlaceholder: string,
currentPopup: string,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
+ issueKey: string,
+ onChange: (Issue) => void,
toggleComment: (open?: boolean, placeholder?: string) => void
-};
+|};
export default class IssueCommentAction extends React.PureComponent {
props: Props;
addComment = (text: string) => {
- this.props.onIssueChange(addIssueComment({ issue: this.props.issueKey, text }));
+ updateIssue(this.props.onChange, addIssueComment({ issue: this.props.issueKey, text }));
this.props.toggleComment(false);
};
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 ab850061b7d..c7cebdf74d7 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
@@ -19,6 +19,7 @@
*/
// @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';
@@ -26,14 +27,14 @@ import { setIssueTags } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';
-type Props = {
+type Props = {|
canSetTags: boolean,
isOpen: boolean,
issue: Issue,
+ onChange: (Issue) => void,
onFail: (Error) => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
-};
+|};
export default class IssueTags extends React.PureComponent {
props: Props;
@@ -45,7 +46,8 @@ export default class IssueTags extends React.PureComponent {
setTags = (tags: Array<string>) => {
const { issue } = this.props;
const newIssue = { ...issue, tags };
- this.props.onIssueChange(
+ updateIssue(
+ this.props.onChange,
setIssueTags({ issue: issue.key, tags: tags.join(',') }),
issue,
newIssue
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 4f847049f54..55ef295f55d 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
@@ -19,23 +19,27 @@
*/
// @flow
import React from 'react';
+import { Link } from 'react-router';
import IssueChangelog from './IssueChangelog';
import IssueMessage from './IssueMessage';
+import SimilarIssuesFilter from './SimilarIssuesFilter';
import { getSingleIssueUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';
-type Props = {
+type Props = {|
issue: Issue,
currentPopup: string,
onFail: (Error) => void,
- onFilterClick?: () => void,
+ onFilter?: (property: string, issue: Issue) => void,
togglePopup: (string) => void
-};
+|};
+
+const stopPropagation = (event: Event) => event.stopPropagation();
export default function IssueTitleBar(props: Props) {
const { issue } = props;
- const hasSimilarIssuesFilter = props.onFilterClick != null;
+ const hasSimilarIssuesFilter = props.onFilter != null;
return (
<table className="issue-table">
@@ -66,21 +70,21 @@ export default function IssueTitleBar(props: Props) {
</span>
</li>}
<li className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href={getSingleIssueUrl(issue.key)}
- target="_blank"
+ onClick={stopPropagation}
+ to={getSingleIssueUrl(issue.key)}
/>
</li>
{hasSimilarIssuesFilter &&
<li className="issue-meta">
- <button
- className="js-issue-filter button-link issue-action issue-action-with-options"
- aria-label={translate('issue.filter_similar_issues')}
- onClick={props.onFilterClick}>
- <i className="icon-filter icon-half-transparent" />{' '}
- <i className="icon-dropdown" />
- </button>
+ <SimilarIssuesFilter
+ isOpen={props.currentPopup === 'similarIssues'}
+ issue={issue}
+ togglePopup={props.togglePopup}
+ onFail={props.onFail}
+ onFilter={props.onFilter}
+ />
</li>}
</ul>
</td>
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 03cd4e41d86..24e3625d529 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
@@ -19,6 +19,7 @@
*/
// @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';
@@ -29,15 +30,20 @@ type Props = {
hasTransitions: boolean,
isOpen: boolean,
issue: Issue,
- setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
+ onChange: (Issue) => void,
togglePopup: (string) => void
};
export default class IssueTransition extends React.PureComponent {
props: Props;
- setTransition = (transition: string) =>
- this.props.setIssueProperty('transition', 'transition', setIssueTransition, transition);
+ setTransition = (transition: string) => {
+ updateIssue(
+ this.props.onChange,
+ setIssueTransition({ issue: this.props.issue.key, transition })
+ );
+ this.toggleSetTransition();
+ };
toggleSetTransition = (open?: boolean) => {
this.props.togglePopup('transition', open);
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
new file mode 100644
index 00000000000..c28593d7c89
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import SimilarIssuesPopup from '../popups/SimilarIssuesPopup';
+import { translate } from '../../../helpers/l10n';
+import type { Issue } from '../types';
+
+type Props = {|
+ isOpen: boolean,
+ issue: Issue,
+ togglePopup: (string) => void,
+ onFail: (Error) => void,
+ onFilter: (property: string, issue: Issue) => void
+|};
+
+export default class SimilarIssuesFilter extends React.PureComponent {
+ props: Props;
+
+ handleClick = (evt: SyntheticInputEvent) => {
+ evt.preventDefault();
+ this.togglePopup();
+ };
+
+ handleFilter = (property: string, issue: Issue) => {
+ this.togglePopup(false);
+ this.props.onFilter(property, issue);
+ };
+
+ togglePopup = (open?: boolean) => {
+ this.props.togglePopup('similarIssues', open);
+ };
+
+ 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>
+ );
+ }
+}
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 ca4a95ff08b..608112423d5 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
@@ -19,7 +19,6 @@
*/
import { shallow } from 'enzyme';
import React from 'react';
-import moment from 'moment';
import IssueChangelog from '../IssueChangelog';
import { click } from '../../../../helpers/testUtils';
@@ -29,7 +28,11 @@ const issue = {
creationDate: '2017-03-01T09:36:01+0100'
};
-moment.fn.fromNow = jest.fn(() => 'a month ago');
+jest.mock('moment', () =>
+ () => ({
+ format: () => 'March 1, 2017 9:36 AM',
+ fromNow: () => 'a month ago'
+ }));
it('should render correctly', () => {
const element = shallow(
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 d681183f2c3..9096b729386 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
@@ -19,7 +19,6 @@
*/
import { shallow } from 'enzyme';
import React from 'react';
-import moment from 'moment';
import IssueCommentLine from '../IssueCommentLine';
import { click } from '../../../../helpers/testUtils';
@@ -32,7 +31,7 @@ const comment = {
updatable: true
};
-moment.fn.fromNow = jest.fn(() => 'a month ago');
+jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' }));
it('should render correctly a comment that is not updatable', () => {
const element = shallow(
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
index 3e110b92f36..1d3b7ac4e0e 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
@@ -43,7 +43,7 @@ it('should render the titlebar with the filter', () => {
issue={issue}
currentPopup=""
onFail={jest.fn()}
- onFilterClick={jest.fn()}
+ onFilter={jest.fn()}
togglePopup={jest.fn()}
/>
);
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 f51811bbd0f..e00752935ca 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
@@ -42,10 +42,19 @@ exports[`test should render the titlebar correctly 1`] = `
</li>
<li
className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
- target="_blank" />
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/issues",
+ "query": Object {
+ "issues": "AVsae-CQS-9G3txfbFN2",
+ },
+ }
+ } />
</li>
</ul>
</td>
@@ -98,23 +107,37 @@ exports[`test should render the titlebar with the filter 1`] = `
</li>
<li
className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
- target="_blank" />
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/issues",
+ "query": Object {
+ "issues": "AVsae-CQS-9G3txfbFN2",
+ },
+ }
+ } />
</li>
<li
className="issue-meta">
- <button
- aria-label="issue.filter_similar_issues"
- className="js-issue-filter button-link issue-action issue-action-with-options"
- onClick={[Function]}>
- <i
- className="icon-filter icon-half-transparent" />
-
- <i
- className="icon-dropdown" />
- </button>
+ <SimilarIssuesFilter
+ isOpen={false}
+ issue={
+ Object {
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "key": "AVsae-CQS-9G3txfbFN2",
+ "line": 26,
+ "message": "Reduce the number of conditional operators (4) used in the expression",
+ "organization": "myorg",
+ "rule": "javascript:S1067",
+ }
+ }
+ onFail={[Function]}
+ onFilter={[Function]}
+ togglePopup={[Function]} />
</li>
</ul>
</td>
diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js
deleted file mode 100644
index e9b4c47cfcd..00000000000
--- a/server/sonar-web/src/main/js/components/issue/issue-view.js
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import ChangeLog from './models/changelog';
-import ChangeLogView from './views/changelog-view';
-import TransitionsFormView from './views/transitions-form-view';
-import AssignFormView from './views/assign-form-view';
-import CommentFormView from './views/comment-form-view';
-import DeleteCommentView from './views/DeleteCommentView';
-import SetSeverityFormView from './views/set-severity-form-view';
-import SetTypeFormView from './views/set-type-form-view';
-import TagsFormView from './views/tags-form-view';
-import Template from './templates/issue.hbs';
-import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- modelEvents: {
- change: 'notifyAndRender',
- transition: 'onTransition'
- },
-
- className() {
- const hasCheckbox = this.options.onCheck != null;
- return hasCheckbox ? 'issue issue-with-checkbox' : 'issue';
- },
-
- events() {
- return {
- click: 'handleClick',
- 'click .js-issue-comment': 'onComment',
- 'click .js-issue-comment-edit': 'editComment',
- 'click .js-issue-comment-delete': 'deleteComment',
- 'click .js-issue-transition': 'transition',
- 'click .js-issue-set-severity': 'setSeverity',
- 'click .js-issue-set-type': 'setType',
- 'click .js-issue-assign': 'assign',
- 'click .js-issue-assign-to-me': 'assignToMe',
- 'click .js-issue-plan': 'plan',
- 'click .js-issue-show-changelog': 'showChangeLog',
- 'click .js-issue-rule': 'showRule',
- 'click .js-issue-edit-tags': 'editTags',
- 'click .js-issue-locations': 'showLocations',
- 'click .js-issue-filter': 'filterSimilarIssues',
- 'click .js-toggle': 'onIssueCheck',
- 'click .js-issue-permalink': 'onPermalinkClick'
- };
- },
-
- notifyAndRender() {
- const { onIssueChange } = this.options;
- if (onIssueChange) {
- onIssueChange(this.model.toJSON());
- }
-
- // if ConnectedIssue is used, this view can be destroyed just after onIssueChange()
- if (!this.isDestroyed) {
- this.render();
- }
- },
-
- onRender() {
- this.$el.attr('data-key', this.model.get('key'));
- },
-
- disableControls() {
- this.$(':input').prop('disabled', true);
- },
-
- enableControls() {
- this.$(':input').prop('disabled', false);
- },
-
- resetIssue(options) {
- const that = this;
- const key = this.model.get('key');
- const componentUuid = this.model.get('componentUuid');
- this.model.reset({ key, componentUuid }, { silent: true });
- return this.model.fetch(options).done(() => that.trigger('reset'));
- },
-
- showChangeLog(e) {
- e.preventDefault();
- e.stopPropagation();
- const that = this;
- const t = $(e.currentTarget);
- const changeLog = new ChangeLog();
- return changeLog
- .fetch({
- data: { issue: this.model.get('key') }
- })
- .done(() => {
- if (that.popup) {
- that.popup.destroy();
- }
- that.popup = new ChangeLogView({
- triggerEl: t,
- bottomRight: true,
- collection: changeLog,
- issue: that.model
- });
- that.popup.render();
- });
- },
-
- updateAfterAction(response) {
- if (this.popup) {
- this.popup.destroy();
- }
- if (response) {
- this.model.set(this.model.parse(response));
- }
- },
-
- onComment(e) {
- e.stopPropagation();
- this.comment();
- },
-
- comment(options) {
- $('body').click();
- this.popup = new CommentFormView({
- triggerEl: this.$('.js-issue-comment'),
- bottom: true,
- issue: this.model,
- detailView: this,
- additionalOptions: options
- });
- this.popup.render();
- },
-
- editComment(e) {
- e.stopPropagation();
- $('body').click();
- const commentEl = $(e.currentTarget).closest('.issue-comment');
- const commentKey = commentEl.data('comment-key');
- const comment = this.model.get('comments').find(comment => comment.key === commentKey);
- this.popup = new CommentFormView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: new Backbone.Model(comment),
- issue: this.model,
- detailView: this
- });
- this.popup.render();
- },
-
- deleteComment(e) {
- e.stopPropagation();
- $('body').click();
- const commentEl = $(e.currentTarget).closest('.issue-comment');
- const commentKey = commentEl.data('comment-key');
- this.popup = new DeleteCommentView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- onDelete: () => {
- this.disableControls();
- $.ajax({
- type: 'POST',
- url: window.baseUrl + '/api/issues/delete_comment?key=' + commentKey
- }).done(r => this.updateAfterAction(r));
- }
- });
- this.popup.render();
- },
-
- transition(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new TransitionsFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model,
- view: this
- });
- this.popup.render();
- },
-
- setSeverity(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new SetSeverityFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- setType(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new SetTypeFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- assign(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new AssignFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- assignToMe() {
- const view = new AssignFormView({
- model: this.model,
- triggerEl: $('body')
- });
- const currentUser = getCurrentUserFromStore();
- view.submit(currentUser.login, currentUser.name);
- view.destroy();
- },
-
- showRule(e) {
- e.preventDefault();
- e.stopPropagation();
- const ruleKey = this.model.get('rule');
- // lazy load Workspace
- const Workspace = require('../workspace/main').default;
- Workspace.openRule({ key: ruleKey, organization: this.model.get('projectOrganization') });
- },
-
- action(action) {
- this.disableControls();
- return this.model
- .customAction(action)
- .done(r => this.updateAfterAction(r))
- .fail(() => this.enableControls());
- },
-
- editTags(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new TagsFormView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: this.model
- });
- this.popup.render();
- },
-
- showLocations() {
- this.model.trigger('locations', this.model);
- },
-
- select() {
- this.$el.addClass('selected');
- },
-
- unselect() {
- this.$el.removeClass('selected');
- },
-
- onTransition(transition) {
- if (transition === 'falsepositive' || transition === 'wontfix') {
- this.comment({ fromTransition: true });
- }
- },
-
- handleClick(e) {
- e.preventDefault();
- const { onClick } = this.options;
- if (onClick) {
- onClick(this.model.get('key'));
- }
- },
-
- filterSimilarIssues(e) {
- this.options.onFilterClick(e);
- },
-
- onIssueCheck(e) {
- this.options.onCheck(e);
- },
-
- onPermalinkClick(e) {
- e.stopPropagation();
- },
-
- serializeData() {
- const issueKey = encodeURIComponent(this.model.get('key'));
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- permalink: window.baseUrl + '/issues/search#issues=' + issueKey,
- hasSecondaryLocations: this.model.get('flows').length,
- hasSimilarIssuesFilter: this.options.onFilterClick != null,
- hasCheckbox: this.options.onCheck != null,
- checked: this.options.checked
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/models/issue.js b/server/sonar-web/src/main/js/components/issue/models/issue.js
deleted file mode 100644
index 1abeee02e24..00000000000
--- a/server/sonar-web/src/main/js/components/issue/models/issue.js
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
- idAttribute: 'key',
-
- defaults() {
- return {
- flows: []
- };
- },
-
- url() {
- return window.baseUrl + '/api/issues';
- },
-
- urlRoot() {
- return window.baseUrl + '/api/issues';
- },
-
- parse(r) {
- let issue = Array.isArray(r.issues) && r.issues.length > 0 ? r.issues[0] : r.issue;
- if (issue) {
- issue = this._injectRelational(issue, r.components, 'component', 'key');
- issue = this._injectRelational(issue, r.components, 'project', 'key');
- issue = this._injectRelational(issue, r.components, 'subProject', 'key');
- issue = this._injectRelational(issue, r.rules, 'rule', 'key');
- issue = this._injectRelational(issue, r.users, 'assignee', 'login');
- issue = this._injectCommentsRelational(issue, r.users);
- issue = this._prepareClosed(issue);
- issue = this.ensureTextRange(issue);
- return issue;
- } else {
- return r;
- }
- },
-
- _injectRelational(issue, source, baseField, lookupField) {
- const baseValue = issue[baseField];
- if (baseValue != null && Array.isArray(source) && source.length > 0) {
- const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
- if (lookupValue != null) {
- Object.keys(lookupValue).forEach(key => {
- const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
- issue[newKey] = lookupValue[key];
- });
- }
- }
- return issue;
- },
-
- _injectCommentsRelational(issue, users) {
- if (issue.comments) {
- const newComments = issue.comments.map(comment => {
- let newComment = { ...comment, author: comment.login };
- delete newComment.login;
- newComment = this._injectRelational(newComment, users, 'author', 'login');
- return newComment;
- });
- return { ...issue, comments: newComments };
- }
- return issue;
- },
-
- _prepareClosed(issue) {
- if (issue.status === 'CLOSED') {
- issue.flows = [];
- delete issue.textRange;
- }
- return issue;
- },
-
- ensureTextRange(issue) {
- if (issue.line && !issue.textRange) {
- // FIXME 999999
- issue.textRange = {
- startLine: issue.line,
- endLine: issue.line,
- startOffset: 0,
- endOffset: 999999
- };
- }
- return issue;
- },
-
- sync(method, model, options) {
- const opts = options || {};
- opts.contentType = 'application/x-www-form-urlencoded';
- if (method === 'read') {
- Object.assign(opts, {
- type: 'GET',
- url: this.urlRoot() + '/search',
- data: {
- issues: model.id,
- additionalFields: '_all'
- }
- });
- }
- if (method === 'create') {
- Object.assign(opts, {
- type: 'POST',
- url: this.urlRoot() + '/create',
- data: {
- component: model.get('component'),
- line: model.get('line'),
- message: model.get('message'),
- rule: model.get('rule'),
- severity: model.get('severity')
- }
- });
- }
- const xhr = (options.xhr = Backbone.ajax(opts));
- model.trigger('request', model, xhr, opts);
- return xhr;
- },
-
- /**
- * Reset issue attributes (delete old, replace with new)
- * @param attrs
- * @param options
- * @returns {Object}
- */
- reset(attrs, options) {
- for (const key in this.attributes) {
- if (this.attributes.hasOwnProperty(key) && !(key in attrs)) {
- attrs[key] = void 0;
- }
- }
- return this.set(attrs, options);
- },
-
- /**
- * Do an action over an issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- * @private
- */
- _action(options) {
- const that = this;
- const success = function(r) {
- const attrs = that.parse(r);
- that.reset(attrs);
- if (options.success) {
- options.success(that, r, options);
- }
- };
- const opts = { type: 'POST', ...options, success };
- const xhr = (options.xhr = Backbone.ajax(opts));
- this.trigger('request', this, xhr, opts);
- return xhr;
- },
-
- /**
- * Assign issue
- * @param {String|null} assignee Assignee, can be null to unassign issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- assign(assignee, options) {
- const opts = {
- url: this.urlRoot() + '/assign',
- data: { issue: this.id, assignee },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Plan issue
- * @param {String|null} plan Action Plan, can be null to unplan issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- plan(plan, options) {
- const opts = {
- url: this.urlRoot() + '/plan',
- data: { issue: this.id, plan },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Set severity of issue
- * @param {String|null} severity Severity
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- setSeverity(severity, options) {
- const opts = {
- url: this.urlRoot() + '/set_severity',
- data: { issue: this.id, severity },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Do transition on issue
- * @param {String|null} transition Transition
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- transition(transition, options) {
- const that = this;
- const opts = {
- url: this.urlRoot() + '/do_transition',
- data: { issue: this.id, transition },
- ...options
- };
- return this._action(opts).done(() => {
- that.trigger('transition', transition);
- });
- },
-
- /**
- * Set type of issue
- * @param {String|null} issueType Issue type
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- setType(issueType, options) {
- const opts = {
- url: this.urlRoot() + '/set_type',
- data: { issue: this.id, type: issueType },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Do a custom (plugin) action
- * @param {String} actionKey Action Key
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- customAction(actionKey, options) {
- const opts = {
- type: 'POST',
- url: this.urlRoot() + '/do_action',
- data: { issue: this.id, actionKey },
- ...options
- };
- const xhr = Backbone.ajax(opts);
- this.trigger('request', this, xhr, opts);
- return xhr;
- },
-
- getLinearLocations() {
- const textRange = this.get('textRange');
- if (!textRange) {
- return [];
- }
- const locations = [];
- for (let line = textRange.startLine; line <= textRange.endLine; line++) {
- // TODO fix 999999
- const from = line === textRange.startLine ? textRange.startOffset : 0;
- const to = line === textRange.endLine ? textRange.endOffset : 999999;
- locations.push({ line, from, to });
- }
- return locations;
- }
-});
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
new file mode 100644
index 00000000000..88d352e9950
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
@@ -0,0 +1,137 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import BubblePopup from '../../../components/common/BubblePopup';
+import SelectList from '../../../components/common/SelectList';
+import SelectListItem from '../../../components/common/SelectListItem';
+import SeverityHelper from '../../../components/shared/SeverityHelper';
+import StatusHelper from '../../../components/shared/StatusHelper';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+import { fileFromPath, limitComponentName } from '../../../helpers/path';
+import type { Issue } from '../types';
+
+type Props = {|
+ issue: Issue,
+ onFilter: (property: string, issue: Issue) => void,
+ popupPosition?: {}
+|};
+
+export default class SimilarIssuesPopup extends React.PureComponent {
+ props: Props;
+
+ handleSelect = (property: string) => {
+ this.props.onFilter(property, this.props.issue);
+ };
+
+ render() {
+ const { issue } = this.props;
+
+ const items = [
+ 'type',
+ 'severity',
+ 'status',
+ 'resolution',
+ 'assignee',
+ 'rule',
+ ...(issue.tags || []).map(tag => `tag###${tag}`),
+ 'project',
+ // $FlowFixMe items are filtered later
+ issue.subProject ? 'module' : undefined,
+ 'file'
+ ].filter(item => item);
+
+ return (
+ <BubblePopup
+ position={this.props.popupPosition}
+ customClass="bubble-popup-menu bubble-popup-bottom-right">
+ <header className="menu-search">
+ <h6>{translate('issue.filter_similar_issues')}</h6>
+ </header>
+
+ <SelectList currentItem={items[0]} items={items} onSelect={this.handleSelect}>
+ <SelectListItem item="type">
+ <IssueTypeIcon className="little-spacer-right" query={issue.type} />
+ {translate('issue.type', issue.type)}
+ </SelectListItem>
+
+ <SelectListItem item="severity">
+ <SeverityHelper severity={issue.severity} />
+ </SelectListItem>
+
+ <SelectListItem item="status">
+ <StatusHelper status={issue.status} />
+ </SelectListItem>
+
+ <SelectListItem item="resolution">
+ {issue.resolution != null
+ ? translate('issue.resolution', issue.resolution)
+ : translate('unresolved')}
+ </SelectListItem>
+
+ <SelectListItem item="assignee">
+ {issue.assignee != null
+ ? <span>
+ {translate('assigned_to')}
+ <Avatar
+ className="little-spacer-left little-spacer-right"
+ hash={issue.assigneeAvatar}
+ size={16}
+ />
+ {issue.assigneeName}
+ </span>
+ : translate('unassigned')}
+ </SelectListItem>
+
+ <SelectListItem item="rule">
+ {limitComponentName(issue.ruleName)}
+ </SelectListItem>
+
+ {issue.tags != null &&
+ issue.tags.map(tag => (
+ <SelectListItem key={`tag###${tag}`} item={`tag###${tag}`}>
+ <i className="icon-tags icon-half-transparent little-spacer-right" />
+ {tag}
+ </SelectListItem>
+ ))}
+
+ <SelectListItem item="project">
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {issue.projectName}
+ </SelectListItem>
+
+ {issue.subProject != null &&
+ <SelectListItem item="module">
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ {issue.subProjectName}
+ </SelectListItem>}
+
+ <SelectListItem item="file">
+ <QualifierIcon className="little-spacer-right" qualifier={issue.componentQualifier} />
+ {fileFromPath(issue.componentLongName)}
+ </SelectListItem>
+ </SelectList>
+ </BubblePopup>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
index 6c4f9d5977e..35d5c05b5f2 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
@@ -21,6 +21,8 @@ import { shallow } from 'enzyme';
import React from 'react';
import ChangelogPopup from '../ChangelogPopup';
+jest.mock('moment', () => () => ({ format: () => 'March 1, 2017 9:36 AM' }));
+
it('should render the changelog popup correctly', () => {
const element = shallow(
<ChangelogPopup
diff --git a/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs b/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs
deleted file mode 100644
index 939bf523509..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="text-right">
- <div class="spacer-bottom">{{t 'issue.comment.delete_confirm_message'}}</div>
- <button class="button-red">{{t 'delete'}}</button>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs
deleted file mode 100644
index d88b8a7da8d..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs
+++ /dev/null
@@ -1,18 +0,0 @@
-<div class="issue-comment-form-text">
- <textarea rows="2" {{#if options.fromTransition}}placeholder="Please tell why?"{{/if}}>{{show raw markdown}}</textarea>
-</div>
-
-<div class="issue-comment-form-footer">
- <div class="issue-comment-form-actions">
- <div class="button-group">
- <button class="js-issue-comment-submit" disabled>
- {{#if id}}{{t 'save'}}{{else}}{{t 'issue.comment.submit'}}{{/if}}
- </button>
- </div>
- <a class="js-issue-comment-cancel">{{t 'cancel'}}</a>
- </div>
-
- <div class="issue-comment-form-tips">{{> ../../common/templates/_markdown-tips }}</div>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs
deleted file mode 100644
index 29550cde4da..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs
+++ /dev/null
@@ -1,5 +0,0 @@
-<li>
- <a href="#" class="js-issue-assignee" data-value="{{id}}" data-text="{{text}}">
- {{text}}
- </a>
-</li>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs
deleted file mode 100644
index 64d2d0d7166..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="search-box menu-search">
- <button class="search-box-submit button-clean">
- <i class="icon-search-new"></i>
- </button>
- <input class="search-box-input" type="search" placeholder="{{t 'search_verb'}}" value="{{query}}">
-</div>
-
-<ul class="menu"></ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs
deleted file mode 100644
index 7ba2e7c2937..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs
+++ /dev/null
@@ -1,37 +0,0 @@
-<div class="issue-changelog">
- <table class="spaced">
- <tbody>
-
- <tr>
- <td class="thin text-left text-top" nowrap>{{dt issue.creationDate}}</td>
- <td class="thin text-left text-top" nowrap></td>
- <td class="text-left text-top">
- {{#if issue.author}}
- {{t 'created_by'}} {{issue.author}}
- {{else}}
- {{t 'created'}}
- {{/if}}
- </td>
- </tr>
-
- {{#each items}}
- <tr>
- <td class="thin text-left text-top" nowrap>{{dt creationDate}}</td>
- <td class="thin text-left text-top" nowrap>
- {{#if userName}}
- {{#ifShowAvatars}}{{avatarHelperNew avatar 16}}{{/ifShowAvatars}}
- {{/if}}
- {{userName}}
- </td>
- <td class="text-left text-top">
- {{#each diffs}}
- {{changelog this}}<br>
- {{/each}}
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs
deleted file mode 100644
index 9184dd34b64..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-<ul class="menu">
- {{#each items}}
- {{#notEq status 'CLOSED'}}
- <li>
- <a href="#" class="js-issue-assignee" data-value="{{key}}" data-text="{{name}}">
- {{name}}
- </a>
- </li>
- {{/notEq}}
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs
deleted file mode 100644
index ea6a3a92b15..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-<ul class="menu">
- {{#each items}}
- <li>
- <a href="#" class="js-issue-severity" data-value="{{this}}">
- {{severityIcon this}} {{t 'severity' this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs
deleted file mode 100644
index 3f42921aba2..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-<ul class="menu">
- {{#each items}}
- <li>
- <a href="#" class="js-issue-type" data-value="{{this}}">
- {{issueType this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs
deleted file mode 100644
index 90df7aa6e62..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs
+++ /dev/null
@@ -1,17 +0,0 @@
-<li>
- <a href="#" data-value="{{tag}}" data-text="{{tag}}"
- {{#if selected}}data-selected{{/if}}>
-
- {{#if selected}}
- <i class="icon-checkbox icon-checkbox-checked"></i>
- {{else}}
- <i class="icon-checkbox"></i>
- {{/if}}
-
- {{#if custom}}
- + {{tag}}
- {{else}}
- {{tag}}
- {{/if}}
- </a>
-</li>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs
deleted file mode 100644
index 64d2d0d7166..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="search-box menu-search">
- <button class="search-box-submit button-clean">
- <i class="icon-search-new"></i>
- </button>
- <input class="search-box-input" type="search" placeholder="{{t 'search_verb'}}" value="{{query}}">
-</div>
-
-<ul class="menu"></ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs
deleted file mode 100644
index ef8ae2f24af..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-<ul class="menu">
- {{#each transitions}}
- <li>
- <a href="#" class="js-issue-transition" data-value="{{this}}"
- title="{{t 'issue.transition' this 'description'}}" data-placement="right" data-container="body">
- {{t 'issue.transition' this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs
deleted file mode 100644
index 72e59a58a1f..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs
+++ /dev/null
@@ -1,182 +0,0 @@
-<div class="issue-inner">
-
- <table class="issue-table">
- <tr>
- <td>
- <div class="issue-message">
- {{message}}&nbsp;
- <button class="button-link js-issue-rule issue-rule icon-ellipsis-h"
- aria-label="{{t 'issue.rule_details'}}"></button>
- </div>
- </td>
-
- <td class="issue-table-meta-cell issue-table-meta-cell-first">
- <ul class="list-inline issue-meta-list">
- <li class="issue-meta">
- <button class="button-link issue-action issue-action-with-options js-issue-show-changelog" title="{{dt creationDate}}">
- <span class="issue-meta-label">{{fromNow creationDate}}</span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- </li>
-
- {{#if line}}
- <li class="issue-meta">
- <span class="issue-meta-label" title="{{t 'line_number'}}">L{{line}}</span>
- </li>
- {{/if}}
-
- {{#if hasSecondaryLocations}}
- <li class="issue-meta issue-meta-locations">
- <button class="button-link issue-action js-issue-locations">
- <i class="icon-issue-flow"></i>
- </button>
- </li>
- {{/if}}
-
- <li class="issue-meta">
- <a class="js-issue-permalink icon-link" href="{{permalink}}" target="_blank"></a>
- </li>
-
- {{#if hasSimilarIssuesFilter}}
- <li class="issue-meta">
- <button class="button-link issue-action issue-action-with-options js-issue-filter"
- aria-label="{{t "issue.filter_similar_issues"}}">
- <i class="icon-filter icon-half-transparent"></i>&nbsp;<i class="icon-dropdown"></i>
- </button>
- </li>
- {{/if}}
- </ul>
- </td>
- </tr>
- </table>
-
- <table class="issue-table">
- <tr>
- <td>
- <ul class="list-inline issue-meta-list">
- <li class="issue-meta">
- {{#inArray actions "set_severity"}}
- <button class="button-link issue-action issue-action-with-options js-issue-set-type">
- {{issueTypeIcon this.type}} {{issueType this.type}}&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- {{issueTypeIcon this.type}} {{issueType this.type}}
- {{/inArray}}
- </li>
-
- <li class="issue-meta">
- {{#inArray actions "set_severity"}}
- <button class="button-link issue-action issue-action-with-options js-issue-set-severity">
- <span class="issue-meta-label">{{severityHelper severity}}</span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- {{severityHelper severity}}
- {{/inArray}}
- </li>
-
- <li class="issue-meta">
- {{#notEmpty transitions}}
- <button class="button-link issue-action issue-action-with-options js-issue-transition">
- <span class="issue-meta-label">{{statusHelper status resolution}}</span>&nbsp;<i
- class="icon-dropdown"></i>
- </button>
- {{else}}
- {{statusHelper status resolution}}
- {{/notEmpty}}
- </li>
-
- <li class="issue-meta">
- {{#inArray actions "assign"}}
- <button class="button-link issue-action issue-action-with-options js-issue-assign">
- {{#if assignee}}
- {{#ifShowAvatars}}
- <span class="text-top">{{avatarHelperNew assigneeAvatar 16}}</span>
- {{/ifShowAvatars}}
- {{/if}}
- <span class="issue-meta-label">{{#if assignee}}{{assigneeName}}{{else}}{{t 'unassigned'}}{{/if}}</span>&nbsp;<i
- class="icon-dropdown"></i>
- </button>
- {{else}}
- {{#if assignee}}
- {{#ifShowAvatars}}
- <span class="text-top">{{avatarHelperNew assigneeAvatar 16}}</span>
- {{/ifShowAvatars}}
- {{/if}}
- <span class="issue-meta-label">{{#if assignee}}{{assigneeName}}{{else}}{{t 'unassigned'}}{{/if}}</span>
- {{/inArray}}
- </li>
-
- {{#if debt}}
- <li class="issue-meta">
- <span class="issue-meta-label">
- {{tp 'issue.x_effort' debt}}
- </span>
- </li>
- {{/if}}
-
- {{#inArray actions "comment"}}
- <li class="issue-meta">
- <button class="button-link issue-action js-issue-comment"><span
- class="issue-meta-label">{{t 'issue.comment.formlink' }}</span></button>
- </li>
- {{/inArray}}
- </ul>
-
- {{#inArray actions "assign_to_me"}}
- <button class="button-link hidden js-issue-assign-to-me"></button>
- {{/inArray}}
- </td>
-
- <td class="issue-table-meta-cell">
- <ul class="list-inline">
- <li class="issue-meta js-issue-tags">
- {{#inArray actions "set_tags"}}
- <button class="button-link issue-action issue-action-with-options js-issue-edit-tags">
- <span>
- <i class="icon-tags icon-half-transparent"></i>&nbsp;<span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
- </span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- <span>
- <i class="icon-tags icon-half-transparent"></i>&nbsp;<span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
- </span>
- {{/inArray}}
- </li>
- </ul>
- </td>
- </tr>
- </table>
-
- {{#notEmpty comments}}
- <div class="issue-comments">
- {{#each comments}}
- <div class="issue-comment" data-comment-key="{{key}}">
- <div class="issue-comment-author" title="{{authorName}}">
- {{#ifShowAvatars}}{{avatarHelperNew authorAvatar 16}}{{else}}
- <i class="icon-comment icon-half-transparent"></i>{{/ifShowAvatars}}&nbsp;{{authorName}}
- </div>
- <div class="issue-comment-text markdown">{{{show html htmlText}}}</div>
- <div class="issue-comment-age">({{fromNow createdAt}})</div>
- <div class="issue-comment-actions">
- {{#if updatable}}
- <button class="js-issue-comment-edit button-link icon-edit icon-half-transparent"></button>
- <button class="js-issue-comment-delete button-link icon-delete icon-half-transparent"
- data-confirm-msg="{{t 'issue.comment.delete_confirm_message'}}"></button>
- {{/if}}
- </div>
- </div>
- {{/each}}
- </div>
- {{/notEmpty}}
-
-</div>
-
-<a class="issue-navigate js-issue-navigate">
- <i class="issue-navigate-to-left icon-chevron-left"></i>
- <i class="issue-navigate-to-right icon-chevron-right"></i>
-</a>
-
-{{#if hasCheckbox}}
- <div class="js-toggle issue-checkbox-container">
- <i class="issue-checkbox icon-checkbox {{#if checked}}icon-checkbox-checked{{/if}}"></i>
- </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js
index 690c38146cb..4a07b129eeb 100644
--- a/server/sonar-web/src/main/js/components/issue/types.js
+++ b/server/sonar-web/src/main/js/components/issue/types.js
@@ -52,6 +52,10 @@ export type Issue = {
assigneeName?: string,
author?: string,
comments?: Array<IssueComment>,
+ component: string,
+ componentLongName: string,
+ componentQualifier: string,
+ componentUuid: string,
creationDate: string,
effort?: string,
key: string,
@@ -61,11 +65,18 @@ export type Issue = {
line?: number,
message: string,
organization: string,
+ project: string,
+ projectName: string,
projectOrganization: string,
+ projectUuid: string,
resolution?: string,
rule: string,
+ ruleName: string,
severity: string,
status: string,
+ subProject?: string,
+ subProjectName?: string,
+ subProjectUuid?: string,
tags?: Array<string>,
textRange: TextRange,
transitions?: Array<string>,
diff --git a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js b/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js
deleted file mode 100644
index 3360de2f416..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import PopupView from '../../common/popup';
-import Template from '../templates/DeleteComment.hbs';
-
-export default PopupView.extend({
- template: Template,
-
- events: {
- 'click button': 'handleSubmit'
- },
-
- handleSubmit(e) {
- e.preventDefault();
- this.options.onDelete();
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js
deleted file mode 100644
index a3e81ef0dae..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import { debounce, uniqBy } from 'lodash';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-assign-form.hbs';
-import OptionTemplate from '../templates/issue-assign-form-option.hbs';
-import { translate } from '../../../helpers/l10n';
-import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore';
-import { areThereCustomOrganizations } from '../../../store/organizations/utils';
-
-export default ActionOptionsView.extend({
- template: Template,
- optionTemplate: OptionTemplate,
-
- events() {
- return {
- ...ActionOptionsView.prototype.events.apply(this, arguments),
- 'click input': 'onInputClick',
- 'keydown input': 'onInputKeydown',
- 'keyup input': 'onInputKeyup'
- };
- },
-
- initialize() {
- ActionOptionsView.prototype.initialize.apply(this, arguments);
- this.assignees = null;
- this.organizationKey = areThereCustomOrganizations()
- ? this.model.get('projectOrganization')
- : null;
- this.debouncedSearch = debounce(this.search, 250);
- },
-
- getAssignee() {
- return this.model.get('assignee');
- },
-
- getAssigneeName() {
- return this.model.get('assigneeName');
- },
-
- onRender() {
- const that = this;
- ActionOptionsView.prototype.onRender.apply(this, arguments);
- this.renderTags();
- setTimeout(
- () => {
- that.$('input').focus();
- },
- 100
- );
- },
-
- renderTags() {
- this.$('.menu').empty();
- this.getAssignees().forEach(this.renderAssignee, this);
- this.bindUIElements();
- this.selectInitialOption();
- },
-
- renderAssignee(assignee) {
- const html = this.optionTemplate(assignee);
- this.$('.menu').append(html);
- },
-
- selectOption(e) {
- const assignee = $(e.currentTarget).data('value');
- const assigneeName = $(e.currentTarget).data('text');
- this.submit(assignee, assigneeName);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(assignee) {
- return this.model.assign(assignee);
- },
-
- onInputClick(e) {
- e.stopPropagation();
- },
-
- onInputKeydown(e) {
- this.query = this.$('input').val();
- if (e.keyCode === 38) {
- this.selectPreviousOption();
- }
- if (e.keyCode === 40) {
- this.selectNextOption();
- }
- if (e.keyCode === 13) {
- this.selectActiveOption();
- }
- if (e.keyCode === 27) {
- this.destroy();
- }
- if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) {
- return false;
- }
- },
-
- onInputKeyup() {
- let query = this.$('input').val();
- if (query !== this.query) {
- if (query.length < 2) {
- query = '';
- }
- this.query = query;
- this.debouncedSearch(query);
- }
- },
-
- search(query) {
- const that = this;
- if (query.length > 1) {
- const searchUrl = this.organizationKey != null
- ? '/organizations/search_members'
- : '/users/search';
- const queryData = { q: query };
- if (this.organizationKey != null) {
- queryData.organization = this.organizationKey;
- }
- $.get(window.baseUrl + '/api' + searchUrl, queryData).done(data => {
- that.resetAssignees(data.users);
- });
- } else {
- this.resetAssignees();
- }
- },
-
- resetAssignees(users) {
- if (users) {
- this.assignees = users.map(user => {
- return { id: user.login, text: user.name };
- });
- } else {
- this.assignees = null;
- }
- this.renderTags();
- },
-
- getAssignees() {
- if (this.assignees) {
- return this.assignees;
- }
- const currentUser = getCurrentUserFromStore();
- const assignees = [
- { id: currentUser.login, text: currentUser.name },
- { id: '', text: translate('unassigned') }
- ];
- return this.makeUnique(assignees);
- },
-
- makeUnique(assignees) {
- return uniqBy(assignees, assignee => assignee.id);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js b/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
deleted file mode 100644
index b04b56abd6a..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import PopupView from '../../common/popup';
-import Template from '../templates/issue-changelog.hbs';
-
-export default PopupView.extend({
- template: Template,
-
- collectionEvents: {
- sync: 'render'
- },
-
- serializeData() {
- return {
- ...PopupView.prototype.serializeData.apply(this, arguments),
- issue: this.options.issue.toJSON()
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js b/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js
deleted file mode 100644
index 52d68bcd7c0..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import PopupView from '../../common/popup';
-import Template from '../templates/comment-form.hbs';
-
-export default PopupView.extend({
- className: 'bubble-popup issue-comment-bubble-popup',
- template: Template,
-
- ui: {
- textarea: '.issue-comment-form-text textarea',
- cancelButton: '.js-issue-comment-cancel',
- submitButton: '.js-issue-comment-submit'
- },
-
- events: {
- click: 'onClick',
- 'keydown @ui.textarea': 'onKeydown',
- 'keyup @ui.textarea': 'toggleSubmit',
- 'click @ui.cancelButton': 'cancel',
- 'click @ui.submitButton': 'submit'
- },
-
- onRender() {
- const that = this;
- PopupView.prototype.onRender.apply(this, arguments);
- setTimeout(
- () => {
- that.ui.textarea.focus();
- },
- 100
- );
- },
-
- toggleSubmit() {
- this.ui.submitButton.prop('disabled', this.ui.textarea.val().length === 0);
- },
-
- onClick(e) {
- e.stopPropagation();
- },
-
- onKeydown(e) {
- if (e.keyCode === 27) {
- this.destroy();
- }
- if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
- this.submit();
- }
- },
-
- cancel() {
- this.options.detailView.updateAfterAction();
- },
-
- disableForm() {
- this.$(':input').prop('disabled', true);
- },
-
- enableForm() {
- this.$(':input').prop('disabled', false);
- },
-
- submit() {
- const text = this.ui.textarea.val();
-
- if (!text.length) {
- return;
- }
-
- const update = this.model && this.model.has('key');
- const method = update ? 'edit_comment' : 'add_comment';
- const url = window.baseUrl + '/api/issues/' + method;
- const data = { text };
- if (update) {
- data.key = this.model.get('key');
- } else {
- data.issue = this.options.issue.id;
- }
- this.disableForm();
- this.options.detailView.disableControls();
- $.post(url, data).done(r => this.options.detailView.updateAfterAction(r)).fail(() => {
- this.enableForm();
- this.options.detailView.enableControls();
- });
- },
-
- serializeData() {
- const options = { fromTransition: false, ...this.options.additionalOptions };
- return {
- ...PopupView.prototype.serializeData.apply(this, arguments),
- options
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js b/server/sonar-web/src/main/js/components/issue/views/issue-popup.js
deleted file mode 100644
index 96488cd1e45..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import PopupView from '../../common/popup';
-
-export default PopupView.extend({
- className: 'bubble-popup issue-bubble-popup',
-
- template() {
- return '<div class="bubble-popup-arrow"></div>';
- },
-
- events() {
- return {
- 'click .js-issue-form-cancel': 'destroy'
- };
- },
-
- onRender() {
- PopupView.prototype.onRender.apply(this, arguments);
- this.options.view.$el.appendTo(this.$el);
- this.options.view.render();
- },
-
- onDestroy() {
- this.options.view.destroy();
- },
-
- attachCloseEvents() {}
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js
deleted file mode 100644
index b30c689e1e9..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-set-severity-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- getTransition() {
- return this.model.get('severity');
- },
-
- selectInitialOption() {
- return this.makeActive(this.getOptions().filter(`[data-value="${this.getTransition()}"]`));
- },
-
- selectOption(e) {
- const severity = $(e.currentTarget).data('value');
- this.submit(severity);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(severity) {
- return this.model.setSeverity(severity);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- items: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js
deleted file mode 100644
index 719d679e762..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-set-type-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- getType() {
- return this.model.get('type');
- },
-
- selectInitialOption() {
- return this.makeActive(this.getOptions().filter(`[data-value="${this.getType()}"]`));
- },
-
- selectOption(e) {
- const issueType = $(e.currentTarget).data('value');
- this.submit(issueType);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(issueType) {
- return this.model.setType(issueType);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- items: ['BUG', 'VULNERABILITY', 'CODE_SMELL']
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js b/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js
deleted file mode 100644
index 87b1287bee9..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import { debounce, difference, without } from 'lodash';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-tags-form.hbs';
-import OptionTemplate from '../templates/issue-tags-form-option.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
- optionTemplate: OptionTemplate,
-
- modelEvents: {
- 'change:tags': 'renderTags'
- },
-
- events() {
- return {
- ...ActionOptionsView.prototype.events.apply(this, arguments),
- 'click input': 'onInputClick',
- 'keydown input': 'onInputKeydown',
- 'keyup input': 'onInputKeyup'
- };
- },
-
- initialize() {
- ActionOptionsView.prototype.initialize.apply(this, arguments);
- this.query = '';
- this.tags = [];
- this.selected = 0;
- this.debouncedSearch = debounce(this.search, 250);
- this.requestTags();
- },
-
- requestTags(query) {
- const that = this;
- return $.get(window.baseUrl + '/api/issues/tags', { ps: 10, q: query }).done(data => {
- that.tags = data.tags;
- that.renderTags();
- });
- },
-
- onRender() {
- const that = this;
- ActionOptionsView.prototype.onRender.apply(this, arguments);
- this.renderTags();
- setTimeout(
- () => {
- that.$('input').focus();
- },
- 100
- );
- },
-
- selectInitialOption() {
- this.selected = Math.max(Math.min(this.selected, this.getOptions().length - 1), 0);
- this.makeActive(this.getOptions().eq(this.selected));
- },
-
- filterTags(tags) {
- return tags.filter(tag => tag.indexOf(this.query) !== -1);
- },
-
- renderTags() {
- this.$('.menu').empty();
- this.filterTags(this.getTags()).forEach(this.renderSelectedTag, this);
- this.filterTags(difference(this.tags, this.getTags())).forEach(this.renderTag, this);
- if (
- this.query.length > 0 &&
- this.tags.indexOf(this.query) === -1 &&
- this.getTags().indexOf(this.query) === -1
- ) {
- this.renderCustomTag(this.query);
- }
- this.selectInitialOption();
- },
-
- renderSelectedTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: true,
- custom: false
- });
- return this.$('.menu').append(html);
- },
-
- renderTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: false,
- custom: false
- });
- return this.$('.menu').append(html);
- },
-
- renderCustomTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: false,
- custom: true
- });
- return this.$('.menu').append(html);
- },
-
- selectOption(e) {
- e.preventDefault();
- e.stopPropagation();
- let tags = this.getTags().slice();
- const tag = $(e.currentTarget).data('value');
- if ($(e.currentTarget).data('selected') != null) {
- tags = without(tags, tag);
- } else {
- tags.push(tag);
- }
- this.selected = this.getOptions().index($(e.currentTarget));
- return this.submit(tags);
- },
-
- submit(tags) {
- const that = this;
- const _tags = this.getTags();
- this.model.set({ tags });
- return $.ajax({
- type: 'POST',
- url: window.baseUrl + '/api/issues/set_tags',
- data: {
- key: this.model.id,
- tags: tags.join()
- }
- }).fail(() => that.model.set({ tags: _tags }));
- },
-
- onInputClick(e) {
- e.stopPropagation();
- },
-
- onInputKeydown(e) {
- this.query = this.$('input').val();
- if (e.keyCode === 38) {
- this.selectPreviousOption();
- }
- if (e.keyCode === 40) {
- this.selectNextOption();
- }
- if (e.keyCode === 13) {
- this.selectActiveOption();
- }
- if (e.keyCode === 27) {
- this.destroy();
- }
- if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) {
- return false;
- }
- },
-
- onInputKeyup() {
- const query = this.$('input').val();
- if (query !== this.query) {
- this.query = query;
- this.debouncedSearch(query);
- }
- },
-
- search(query) {
- this.query = query;
- return this.requestTags(query);
- },
-
- resetAssignees(users) {
- this.assignees = users.map(user => {
- return { id: user.login, text: user.name };
- });
- this.renderTags();
- },
-
- getTags() {
- return this.model.get('tags') || [];
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js b/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js
deleted file mode 100644
index 0a44b5a4b22..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-transitions-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- selectInitialOption() {
- this.makeActive(this.getOptions().first());
- },
-
- selectOption(e) {
- const transition = $(e.currentTarget).data('value');
- this.submit(transition);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(transition) {
- return this.model.transition(transition);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js b/server/sonar-web/src/main/js/components/layout/Page.js
index 8d15af06288..a8adef56e19 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
+++ b/server/sonar-web/src/main/js/components/layout/Page.js
@@ -18,12 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import { connect } from 'react-redux';
-import LineIssuesIndicator from './LineIssuesIndicator';
-import { getIssueByKey } from '../../../store/rootReducer';
+import React from 'react';
+import { css } from 'glamor';
-const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({
- issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey))
+type Props = {
+ className?: string,
+ children?: React.Element<*>
+};
+
+const styles = css({
+ display: 'flex',
+ alignItems: 'stretch',
+ width: '100%',
+ flexGrow: 1
});
-export default connect(mapStateToProps)(LineIssuesIndicator);
+const Page = ({ className, children, ...other }: Props) => (
+ <div className={styles + (className ? ` ${className}` : '')} {...other}>
+ {children}
+ </div>
+);
+
+export default Page;
diff --git a/server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js b/server/sonar-web/src/main/js/components/layout/PageFilters.js
index cd9aaf55d66..f969366de69 100644
--- a/server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js
+++ b/server/sonar-web/src/main/js/components/layout/PageFilters.js
@@ -17,6 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-module.exports = function(componentKey) {
- return window.baseUrl + '/component_issues/index?id=' + encodeURIComponent(componentKey);
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+
+type Props = {
+ children?: React.Element<*>
};
+
+const PageSide = (props: Props) => (
+ <div className={css({ width: 260, padding: 20 })}>
+ {props.children}
+ </div>
+);
+
+export default PageSide;
diff --git a/server/sonar-web/src/main/js/components/layout/PageMain.js b/server/sonar-web/src/main/js/components/layout/PageMain.js
new file mode 100644
index 00000000000..6195a1f651a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/layout/PageMain.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+
+type Props = {
+ children?: React.Element<*>
+};
+
+const PageMain = (props: Props) => (
+ <div className={css({ flexGrow: 1, minWidth: 740, padding: 20 })}>
+ {props.children}
+ </div>
+);
+
+export default PageMain;
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
index eea72dc3e28..41beed6518f 100644
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js
+++ b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
@@ -17,13 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import { translate } from '../../helpers/l10n';
+// @flow
+import React from 'react';
+import { css } from 'glamor';
-export default Marionette.ItemView.extend({
- className: 'search-navigator-no-results',
+type Props = {
+ children?: React.Element<*>
+};
- template() {
- return translate('issue_filter.no_issues');
- }
-});
+const PageMainInner = (props: Props) => (
+ <div className={css({ minWidth: 740, maxWidth: 980 })}>
+ {props.children}
+ </div>
+);
+
+export default PageMainInner;
diff --git a/server/sonar-web/src/main/js/components/layout/PageSide.js b/server/sonar-web/src/main/js/components/layout/PageSide.js
new file mode 100644
index 00000000000..0488fbfceb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/layout/PageSide.js
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css, media } from 'glamor';
+
+type Props = {
+ children?: React.Element<*>,
+ top?: number
+};
+
+const width = css(
+ {
+ width: 'calc(50vw - 360px)'
+ },
+ media('(max-width: 1320px)', { width: 300 })
+);
+
+const sideStyles = css(width, {
+ flexGrow: 0,
+ flexShrink: 0,
+ borderRight: '1px solid #e6e6e6',
+ backgroundColor: '#f3f3f3'
+});
+
+const sideStickyStyles = css(width, {
+ position: 'fixed',
+ zIndex: 40,
+ top: 0,
+ bottom: 0,
+ left: 0,
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ backgroundColor: '#f3f3f3'
+});
+
+const sideInnerStyles = css(
+ {
+ width: 300,
+ marginLeft: 'calc(50vw - 660px)',
+ backgroundColor: '#f3f3f3'
+ },
+ media('(max-width: 1320px)', { marginLeft: 0 })
+);
+
+const PageSide = (props: Props) => (
+ <div className={sideStyles}>
+ <div className={sideStickyStyles} style={{ top: props.top || 30 }}>
+ <div className={sideInnerStyles}>
+ {props.children}
+ </div>
+ </div>
+ </div>
+);
+
+export default PageSide;
diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
index d47b6e82ba4..b7b35b539c5 100644
--- a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
+++ b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
@@ -20,6 +20,7 @@
import $ from 'jquery';
import { throttle } from 'lodash';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
const BOTTOM_OFFSET = 60;
diff --git a/server/sonar-web/src/main/js/components/shared/Organization.js b/server/sonar-web/src/main/js/components/shared/Organization.js
index 807abd3d11e..ac723440c9c 100644
--- a/server/sonar-web/src/main/js/components/shared/Organization.js
+++ b/server/sonar-web/src/main/js/components/shared/Organization.js
@@ -29,6 +29,7 @@ type OwnProps = {
type Props = {
link?: boolean,
+ linkClassName?: string,
organizationKey: string,
organization: { key: string, name: string } | null,
shouldBeDisplayed: boolean
@@ -51,7 +52,9 @@ class Organization extends React.Component {
return (
<span>
{this.props.link
- ? <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+ ? <OrganizationLink className={this.props.linkClassName} organization={organization}>
+ {organization.name}
+ </OrganizationLink>
: organization.name}
<span className="slash-separator" />
</span>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js
index c2194ad5409..82ed9f7e5e1 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js
+++ b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js
@@ -18,15 +18,26 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import classNames from 'classnames';
+
+type Props = {
+ className?: string,
+ qualifier: ?string
+};
+
+export default class QualifierIcon extends React.PureComponent {
+ props: Props;
-export default class NoProjects extends React.Component {
render() {
- return (
- <div className="projects-empty-list">
- <h3>{translate('projects.no_projects.1')}</h3>
- <p className="big-spacer-top">{translate('projects.no_projects.2')}</p>
- </div>
+ if (!this.props.qualifier) {
+ return null;
+ }
+
+ const className = classNames(
+ 'icon-qualifier-' + this.props.qualifier.toLowerCase(),
+ this.props.className
);
+
+ return <i className={className} />;
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js
index dd54e82c8d2..857ccbaf450 100644
--- a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js
+++ b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js
@@ -17,24 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-mode-facet.hbs';
+import React from 'react';
+import { shallow } from 'enzyme';
+import QualifierIcon from '../QualifierIcon';
-export default BaseFacet.extend({
- template: Template,
+it('should render icon', () => {
+ expect(shallow(<QualifierIcon qualifier="TRK" />)).toMatchSnapshot();
+ expect(shallow(<QualifierIcon qualifier="trk" />)).toMatchSnapshot();
+});
- toggleFacet(e) {
- const isCount = $(e.currentTarget).is('[data-value="count"]');
- return this.options.app.state.updateFilter({
- facetMode: isCount ? 'count' : 'effort'
- });
- },
+it('should not render icon', () => {
+ expect(shallow(<QualifierIcon qualifier={null} />)).toMatchSnapshot();
+});
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- mode: this.options.app.state.getFacetMode()
- };
- }
+it('should render with custom class', () => {
+ expect(shallow(<QualifierIcon className="spacer-right" qualifier="TRK" />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap
new file mode 100644
index 00000000000..58ac761a183
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap
@@ -0,0 +1,16 @@
+exports[`test should not render icon 1`] = `null`;
+
+exports[`test should render icon 1`] = `
+<i
+ className="icon-qualifier-trk" />
+`;
+
+exports[`test should render icon 2`] = `
+<i
+ className="icon-qualifier-trk" />
+`;
+
+exports[`test should render with custom class 1`] = `
+<i
+ className="icon-qualifier-trk spacer-right" />
+`;
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js b/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js
new file mode 100644
index 00000000000..04654471d59
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { parseIssueFromResponse } from '../issues';
+
+it('should populate comments data', () => {
+ const users = [
+ {
+ active: true,
+ avatar: 'c1244e6857f7be3dc4549d9e9d51c631',
+ login: 'admin',
+ name: 'Admin Admin'
+ }
+ ];
+ const issue = {
+ comments: [
+ {
+ createdAt: '2017-04-11T10:38:09+0200',
+ htmlText: 'comment!',
+ key: 'AVtcKbZkQmGLa7yW8J71',
+ login: 'admin',
+ markdown: 'comment!',
+ updatable: true
+ }
+ ]
+ };
+ expect(parseIssueFromResponse(issue, undefined, users, undefined)).toEqual({
+ comments: [
+ {
+ author: 'admin',
+ authorActive: true,
+ authorAvatar: 'c1244e6857f7be3dc4549d9e9d51c631',
+ authorLogin: 'admin',
+ authorName: 'Admin Admin',
+ createdAt: '2017-04-11T10:38:09+0200',
+ htmlText: 'comment!',
+ key: 'AVtcKbZkQmGLa7yW8J71',
+ login: undefined,
+ markdown: 'comment!',
+ updatable: true
+ }
+ ]
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
index c6e54c559d5..c2047a572a4 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
+++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
@@ -55,32 +55,17 @@ describe('#getComponentUrl', () => {
describe('#getComponentIssuesUrl', () => {
it('should work without parameters', () => {
- expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toBe(
- '/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#'
- );
- });
-
- it('should encode component key', () => {
- expect(getComponentIssuesUrl(COMPLEX_COMPONENT_KEY, {})).toBe(
- '/component_issues?id=' + COMPLEX_COMPONENT_KEY_ENCODED + '#'
- );
+ expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({
+ pathname: '/project/issues',
+ query: { id: SIMPLE_COMPONENT_KEY }
+ });
});
it('should work with parameters', () => {
- expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toBe(
- '/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#resolved=false'
- );
- });
-
- it('should encode parameters', () => {
- expect(
- getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { componentUuids: COMPLEX_COMPONENT_KEY })
- ).toBe(
- '/component_issues?id=' +
- SIMPLE_COMPONENT_KEY +
- '#componentUuids=' +
- COMPLEX_COMPONENT_KEY_ENCODED
- );
+ expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual({
+ pathname: '/project/issues',
+ query: { id: SIMPLE_COMPONENT_KEY, resolved: 'false' }
+ });
});
});
diff --git a/server/sonar-web/src/main/js/helpers/issues.js b/server/sonar-web/src/main/js/helpers/issues.js
index e8ce3a697ec..013831fc1e6 100644
--- a/server/sonar-web/src/main/js/helpers/issues.js
+++ b/server/sonar-web/src/main/js/helpers/issues.js
@@ -20,6 +20,7 @@
// @flow
import { sortBy } from 'lodash';
import { SEVERITIES } from './constants';
+import type { Issue } from '../components/issue/types';
type TextRange = {
startLine: number,
@@ -83,12 +84,13 @@ const injectCommentsRelational = (issue: RawIssue, users?: Array<User>) => {
if (!issue.comments) {
return {};
}
- const comments = issue.comments.map(comment => ({
- ...comment,
- author: comment.login,
- login: undefined,
- ...injectRelational(comment, users, 'author', 'login')
- }));
+ const comments = issue.comments.map(comment => {
+ const commentWithAuthor = { ...comment, author: comment.login, login: undefined };
+ return {
+ ...commentWithAuthor,
+ ...injectRelational(commentWithAuthor, users, 'author', 'login')
+ };
+ });
return { comments };
};
@@ -110,11 +112,11 @@ const ensureTextRange = (issue: RawIssue) => {
};
export const parseIssueFromResponse = (
- issue: RawIssue,
+ issue: Object,
components?: Array<*>,
users?: Array<*>,
rules?: Array<*>
-) => {
+): Issue => {
return {
...issue,
...injectRelational(issue, components, 'component', 'key'),
diff --git a/server/sonar-web/src/main/js/helpers/path.js b/server/sonar-web/src/main/js/helpers/path.js
index 63153bf6dc8..b65c049f3e4 100644
--- a/server/sonar-web/src/main/js/helpers/path.js
+++ b/server/sonar-web/src/main/js/helpers/path.js
@@ -102,3 +102,12 @@ export function splitPath(path) {
return null;
}
}
+
+export function limitComponentName(str) {
+ if (typeof str === 'string') {
+ const LIMIT = 30;
+ return str.length > LIMIT ? str.substr(0, LIMIT) + '...' : str;
+ } else {
+ return '';
+ }
+}
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js
index 602eb3c5786..96b718cc64b 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.js
+++ b/server/sonar-web/src/main/js/helpers/testUtils.js
@@ -24,7 +24,7 @@ export const mockEvent = {
stopPropagation() {}
};
-export const click = element => element.simulate('click', mockEvent);
+export const click = (element, event = {}) => element.simulate('click', { ...mockEvent, ...event });
export const submit = element =>
element.simulate('submit', {
@@ -37,4 +37,7 @@ export const change = (element, value) =>
currentTarget: { value }
});
-export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode });
+export const keydown = keyCode => {
+ const event = new KeyboardEvent('keydown', { keyCode });
+ document.dispatchEvent(event);
+};
diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js
index 34b3b06d89d..f8e127b89a0 100644
--- a/server/sonar-web/src/main/js/helpers/urls.js
+++ b/server/sonar-web/src/main/js/helpers/urls.js
@@ -37,40 +37,23 @@ export function getProjectUrl(key) {
/**
* Generate URL for a global issues page
- * @param {object} query
- * @returns {string}
*/
export function getIssuesUrl(query) {
- const serializedQuery = Object.keys(query)
- .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`)
- .join('|');
-
- // return a string (not { pathname }) to help react-router's Link handle this properly
- return '/issues#' + serializedQuery;
+ return { pathname: '/issues', query };
}
/**
* Generate URL for a component's issues page
- * @param {string} componentKey
- * @param {object} query
- * @returns {string}
*/
export function getComponentIssuesUrl(componentKey, query) {
- const serializedQuery = Object.keys(query)
- .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`)
- .join('|');
-
- // return a string (not { pathname }) to help react-router's Link handle this properly
- return '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery;
+ return { pathname: '/project/issues', query: { ...query, id: componentKey } };
}
/**
* Generate URL for a single issue
- * @param {string} issueKey
- * @returns {string}
*/
-export function getSingleIssueUrl(issueKey) {
- return window.baseUrl + '/issues/search#issues=' + issueKey;
+export function getSingleIssueUrl(issues) {
+ return { pathname: '/issues', query: { issues } };
}
/**
diff --git a/server/sonar-web/src/main/js/libs/third-party/keymaster.js b/server/sonar-web/src/main/js/libs/third-party/keymaster.js
deleted file mode 100644
index 8ba7aad8487..00000000000
--- a/server/sonar-web/src/main/js/libs/third-party/keymaster.js
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// keymaster.js
-// version: 1.6.2
-// (c) 2011-2013 Thomas Fuchs
-// keymaster.js may be freely distributed under the MIT license.
-
-;(function(global){
- var k,
- _handlers = {},
- _mods = { 16: false, 18: false, 17: false, 91: false },
- _scope = 'all',
- // modifier keys
- _MODIFIERS = {
- '⇧': 16, shift: 16,
- '⌥': 18, alt: 18, option: 18,
- '⌃': 17, ctrl: 17, control: 17,
- '⌘': 91, command: 91
- },
- // special keys
- _MAP = {
- backspace: 8, tab: 9, clear: 12,
- enter: 13, 'return': 13,
- esc: 27, escape: 27, space: 32,
- left: 37, up: 38,
- right: 39, down: 40,
- del: 46, 'delete': 46,
- home: 36, end: 35,
- pageup: 33, pagedown: 34,
- ',': 188, '.': 190, '/': 191,
- '`': 192, '-': 189, '=': 187,
- ';': 186, '\'': 222,
- '[': 219, ']': 221, '\\': 220
- },
- code = function(x){
- return _MAP[x] || x.toUpperCase().charCodeAt(0);
- },
- _downKeys = [];
-
- for(k=1;k<20;k++) _MAP['f'+k] = 111+k;
-
- // IE doesn't support Array#indexOf, so have a simple replacement
- function index(array, item){
- var i = array.length;
- while(i--) if(array[i]===item) return i;
- return -1;
- }
-
- // for comparing mods before unassignment
- function compareArray(a1, a2) {
- if (a1.length != a2.length) return false;
- for (var i = 0; i < a1.length; i++) {
- if (a1[i] !== a2[i]) return false;
- }
- return true;
- }
-
- var modifierMap = {
- 16:'shiftKey',
- 18:'altKey',
- 17:'ctrlKey',
- 91:'metaKey'
- };
- function updateModifierKey(event) {
- for(k in _mods) _mods[k] = event[modifierMap[k]];
- };
-
- // handle keydown event
- function dispatch(event) {
- var key, handler, k, i, modifiersMatch, scope;
- key = event.keyCode;
-
- if (index(_downKeys, key) == -1) {
- _downKeys.push(key);
- }
-
- // if a modifier key, set the key.<modifierkeyname> property to true and return
- if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
- if(key in _mods) {
- _mods[key] = true;
- // 'assignKey' from inside this closure is exported to window.key
- for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
- return;
- }
- updateModifierKey(event);
-
- // see if we need to ignore the keypress (filter() can can be overridden)
- // by default ignore key presses if a select, textarea, or input is focused
- if(!assignKey.filter.call(this, event)) return;
-
- // abort if no potentially matching shortcuts found
- if (!(key in _handlers)) return;
-
- scope = getScope();
-
- // for each potential shortcut
- for (i = 0; i < _handlers[key].length; i++) {
- handler = _handlers[key][i];
-
- // see if it's in the current scope
- if(handler.scope == scope || handler.scope == 'all'){
- // check if modifiers match if any
- modifiersMatch = handler.mods.length > 0;
- for(k in _mods)
- if((!_mods[k] && index(handler.mods, +k) > -1) ||
- (_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
- // call the handler and stop the event if neccessary
- if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
- if(handler.method(event, handler)===false){
- if(event.preventDefault) event.preventDefault();
- else event.returnValue = false;
- if(event.stopPropagation) event.stopPropagation();
- if(event.cancelBubble) event.cancelBubble = true;
- }
- }
- }
- }
- };
-
- // unset modifier keys on keyup
- function clearModifier(event){
- var key = event.keyCode, k,
- i = index(_downKeys, key);
-
- // remove key from _downKeys
- if (i >= 0) {
- _downKeys.splice(i, 1);
- }
-
- if(key == 93 || key == 224) key = 91;
- if(key in _mods) {
- _mods[key] = false;
- for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
- }
- };
-
- function resetModifiers() {
- for(k in _mods) _mods[k] = false;
- for(k in _MODIFIERS) assignKey[k] = false;
- };
-
- // parse and assign shortcut
- function assignKey(key, scope, method){
- var keys, mods;
- keys = getKeys(key);
- if (method === undefined) {
- method = scope;
- scope = 'all';
- }
-
- // for each shortcut
- for (var i = 0; i < keys.length; i++) {
- // set modifier keys if any
- mods = [];
- key = keys[i].split('+');
- if (key.length > 1){
- mods = getMods(key);
- key = [key[key.length-1]];
- }
- // convert to keycode and...
- key = key[0]
- key = code(key);
- // ...store handler
- if (!(key in _handlers)) _handlers[key] = [];
- _handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
- }
- };
-
- // unbind all handlers for given key in current scope
- function unbindKey(key, scope) {
- var multipleKeys, keys,
- mods = [],
- i, j, obj;
-
- multipleKeys = getKeys(key);
-
- for (j = 0; j < multipleKeys.length; j++) {
- keys = multipleKeys[j].split('+');
-
- if (keys.length > 1) {
- mods = getMods(keys);
- key = keys[keys.length - 1];
- }
-
- key = code(key);
-
- if (scope === undefined) {
- scope = getScope();
- }
- if (!_handlers[key]) {
- return;
- }
- for (i = 0; i < _handlers[key].length; i++) {
- obj = _handlers[key][i];
- // only clear handlers if correct scope and mods match
- if (obj.scope === scope && compareArray(obj.mods, mods)) {
- _handlers[key][i] = {};
- }
- }
- }
- };
-
- // Returns true if the key with code 'keyCode' is currently down
- // Converts strings into key codes.
- function isPressed(keyCode) {
- if (typeof(keyCode)=='string') {
- keyCode = code(keyCode);
- }
- return index(_downKeys, keyCode) != -1;
- }
-
- function getPressedKeyCodes() {
- return _downKeys.slice(0);
- }
-
- function filter(event){
- var tagName = (event.target || event.srcElement).tagName;
- // ignore keypressed in any elements that support keyboard data input
- return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
- }
-
- // initialize key.<modifier> to false
- for(k in _MODIFIERS) assignKey[k] = false;
-
- // set current scope (default 'all')
- function setScope(scope){ _scope = scope || 'all' };
- function getScope(){ return _scope || 'all' };
-
- // delete all handlers for a given scope
- function deleteScope(scope){
- var key, handlers, i;
-
- for (key in _handlers) {
- handlers = _handlers[key];
- for (i = 0; i < handlers.length; ) {
- if (handlers[i].scope === scope) handlers.splice(i, 1);
- else i++;
- }
- }
- };
-
- // abstract key logic for assign and unassign
- function getKeys(key) {
- var keys;
- key = key.replace(/\s/g, '');
- keys = key.split(',');
- if ((keys[keys.length - 1]) == '') {
- keys[keys.length - 2] += ',';
- }
- return keys;
- }
-
- // abstract mods logic for assign and unassign
- function getMods(key) {
- var mods = key.slice(0, key.length - 1);
- for (var mi = 0; mi < mods.length; mi++)
- mods[mi] = _MODIFIERS[mods[mi]];
- return mods;
- }
-
- // cross-browser events
- function addEvent(object, event, method) {
- if (object.addEventListener)
- object.addEventListener(event, method, false);
- else if(object.attachEvent)
- object.attachEvent('on'+event, function(){ method(window.event) });
- };
-
- // set the handlers globally on document
- addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
- addEvent(document, 'keyup', clearModifier);
-
- // reset modifiers to false whenever the window is (re)focused.
- addEvent(window, 'focus', resetModifiers);
-
- // store previously defined key
- var previousKey = global.key;
-
- // restore previously defined key and return reference to our key object
- function noConflict() {
- var k = global.key;
- global.key = previousKey;
- return k;
- }
-
- // set window.key and window.key.set/get/deleteScope, and the default filter
- global.key = assignKey;
- global.key.setScope = setScope;
- global.key.getScope = getScope;
- global.key.deleteScope = deleteScope;
- global.key.filter = filter;
- global.key.isPressed = isPressed;
- global.key.getPressedKeyCodes = getPressedKeyCodes;
- global.key.noConflict = noConflict;
- global.key.unbind = unbindKey;
-
-})(window);
diff --git a/server/sonar-web/src/main/js/store/issues/duck.js b/server/sonar-web/src/main/js/store/issues/duck.js
deleted file mode 100644
index b559d8103c7..00000000000
--- a/server/sonar-web/src/main/js/store/issues/duck.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import { keyBy } from 'lodash';
-
-type Issue = { key: string };
-
-type ReceiveIssuesAction = {
- type: 'RECEIVE_ISSUES',
- issues: Array<Issue>
-};
-
-type Action = ReceiveIssuesAction;
-
-type State = { [key: string]: Issue };
-
-export const receiveIssues = (issues: Array<Issue>): ReceiveIssuesAction => ({
- type: 'RECEIVE_ISSUES',
- issues
-});
-
-const reducer = (state: State = {}, action: Action) => {
- switch (action.type) {
- case 'RECEIVE_ISSUES':
- return { ...state, ...keyBy(action.issues, 'key') };
- default:
- return state;
- }
-};
-
-export default reducer;
-
-export const getIssueByKey = (state: State, key: string): ?Issue => state[key];
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 95db709a545..5404ef78f6a 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -22,7 +22,6 @@ import appState from './appState/duck';
import components, * as fromComponents from './components/reducer';
import users, * as fromUsers from './users/reducer';
import favorites, * as fromFavorites from './favorites/duck';
-import issues, * as fromIssues from './issues/duck';
import languages, * as fromLanguages from './languages/reducer';
import measures, * as fromMeasures from './measures/reducer';
import notifications, * as fromNotifications from './notifications/duck';
@@ -42,7 +41,6 @@ export default combineReducers({
components,
globalMessages,
favorites,
- issues,
languages,
measures,
notifications,
@@ -85,8 +83,6 @@ export const getUsers = state => fromUsers.getUsers(state.users);
export const isFavorite = (state, componentKey) =>
fromFavorites.isFavorite(state.favorites, componentKey);
-export const getIssueByKey = (state, key) => fromIssues.getIssueByKey(state.issues, key);
-
export const getComponentMeasure = (state, componentKey, metricKey) =>
fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey);
diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less
index 658462e620c..ba09fadce81 100644
--- a/server/sonar-web/src/main/less/components/issues.less
+++ b/server/sonar-web/src/main/less/components/issues.less
@@ -43,7 +43,7 @@
.issue-list,
.issue {
- max-width: 920px;
+ max-width: 980px;
}
.issue.selected {
@@ -91,6 +91,8 @@
}
.issue-rule {
+ vertical-align: top;
+ margin-top: 2px;
padding: 0 3px;
background: fade(@blue, 30%);
opacity: 0.5;
@@ -346,7 +348,7 @@ input.issue-action-options-search {
top: 0;
bottom: 0;
left: 0;
- cursor: pointer;
+ border: none;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less
index 9e24c26252f..cd284d7a546 100644
--- a/server/sonar-web/src/main/less/components/page.less
+++ b/server/sonar-web/src/main/less/components/page.less
@@ -153,10 +153,11 @@
}
.page-sidebar-sticky {
+ width: 320px !important;
padding-right: 0;
.page-limited & {
- margin: -20px 0;
+ margin: -20px 0 -20px -20px;
padding-right: 0 !important;
.page-sidebar-sticky-inner {
@@ -166,16 +167,27 @@
.page-sidebar-sticky-inner {
position: fixed;
+ z-index: 10;
top: 30px;
bottom: 0;
+ left: 0;
overflow: auto;
- width: 290px;
+ width: ~"calc(50vw - 640px + 280px + 3px)";
border-right: 1px solid #e6e6e6;
box-sizing: border-box;
background: #f3f3f3;
+ @media (max-width: 1335px) {
+ & { width: 310px; }
+ }
+
.search-navigator-facets-list {
width: 260px;
+ margin-left: ~"calc(50vw - 640px + 290px - 260px - 37px)";
+
+ @media (max-width: 1335px) {
+ & { margin-left: 20px; }
+ }
}
}
}
diff --git a/server/sonar-web/src/main/less/components/react-select.less b/server/sonar-web/src/main/less/components/react-select.less
index 0f496e61f58..a29d4113e52 100644
--- a/server/sonar-web/src/main/less/components/react-select.less
+++ b/server/sonar-web/src/main/less/components/react-select.less
@@ -101,6 +101,21 @@
white-space: nowrap;
}
+.Select-value svg,
+.Select-value [class^="icon-"] {
+ padding-top: 4px;
+}
+
+.Select-value img {
+ padding-top: 3px;
+}
+
+.Select-option svg,
+.Select-option img,
+.Select-option [class^="icon-"] {
+ padding-top: 2px;
+}
+
.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
color: @baseFontColor;
@@ -311,26 +326,15 @@
padding: 8px 10px;
}
-.Select--multi .Select-input {
- vertical-align: middle;
- margin-left: 10px;
- padding: 0;
-}
-
-.Select--multi.has-value .Select-input {
- margin-left: 5px;
-}
-
.Select--multi .Select-value {
background-color: rgba(0, 126, 255, 0.08);
border-radius: 2px;
border: 1px solid rgba(0, 126, 255, 0.24);
- color: #007eff;
+ color: @baseFontColor;
display: inline-block;
- font-size: 0.9em;
- line-height: 1.4;
- margin-left: 5px;
- margin-top: 5px;
+ font-size: 12px;
+ line-height: 14px;
+ margin: 1px 4px 1px 1px;
vertical-align: top;
}
@@ -400,6 +404,10 @@
background-color: #fcfcfc;
}
+.Select-aria-only {
+ display: none;
+}
+
@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
diff --git a/server/sonar-web/src/main/less/components/search-navigator.less b/server/sonar-web/src/main/less/components/search-navigator.less
index 08d9a90c69a..8b0203bfd2a 100644
--- a/server/sonar-web/src/main/less/components/search-navigator.less
+++ b/server/sonar-web/src/main/less/components/search-navigator.less
@@ -122,23 +122,30 @@
white-space: normal;
overflow: hidden;
font-size: 0;
- cursor: pointer;
transition: none;
- &:hover {
- border: 1px solid @blue;
- padding: 3px 5px;
+ a& {
+ cursor: pointer;
- .facet-stat {
- top: -1px;
- right: -1px;
+ .facet-name {
+ color: @baseFontColor;
+ }
+
+ &:hover, &:focus {
+ border: 1px solid @blue;
+ padding: 3px 5px;
+
+ .facet-stat {
+ top: -1px;
+ right: -1px;
+ }
}
}
.facet-name {
line-height: 16px;
background-color: @barBackgroundColor;
- color: @baseFontColor;
+ color: @secondFontColor;
font-size: @smallFontSize;
white-space: nowrap;
}
@@ -425,6 +432,7 @@
.search-navigator-date-facet-selection {
.clearfix;
position: relative;
+ padding: 0 10px;
font-size: @smallFontSize;
}
diff --git a/server/sonar-web/src/main/less/pages/issues.less b/server/sonar-web/src/main/less/pages/issues.less
index 3c25afc7d90..c6b9336153e 100644
--- a/server/sonar-web/src/main/less/pages/issues.less
+++ b/server/sonar-web/src/main/less/pages/issues.less
@@ -39,10 +39,29 @@
position: absolute;
visibility: hidden;
}
+
+ .search-navigator-facet-header,
+ .search-navigator-facet-list {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .search-navigator-facet-header {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+
+ .search-navigator-facet-box:not(.hidden) + .search-navigator-facet-box {
+ border-top: none;
+ }
+
+ .search-navigator-facet-footer {
+ padding: 0 0 10px 0;
+ }
}
.issues-workspace-list-component {
- padding: 0 10px;
+ padding: 10px 10px 6px;
}
.issues-workspace-list-item + .issues-workspace-list-item {
@@ -53,8 +72,12 @@
margin-top: 10px;
}
-.issues-workspace-list-item + .issues-workspace-list-component {
- margin-top: 25px;
+.issues-workspace-list-item:first-child .issues-workspace-list-component {
+ padding-top: 0;
+}
+
+.issues-workspace-list-component + .issues-workspace-list-item {
+ margin-top: 0;
}
.issues-workspace-component-viewer {