]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9064 Rework facets sidebar on the issues page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 3 Apr 2017 15:56:23 +0000 (17:56 +0200)
committerStas Vilchik <stas-vilchik@users.noreply.github.com>
Thu, 13 Apr 2017 10:21:37 +0000 (12:21 +0200)
273 files changed:
it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java
it/it-tests/src/test/java/it/issue/IssueSearchTest.java
it/it-tests/src/test/java/it/ui/UiTest.java
it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java
it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html [deleted file]
it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt
server/sonar-web/.eslintrc
server/sonar-web/config/webpack/webpack.config.base.js
server/sonar-web/package.json
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/api/issues.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
server/sonar-web/src/main/js/apps/code/components/ComponentName.js
server/sonar-web/src/main/js/apps/coding-rules/controller.js
server/sonar-web/src/main/js/apps/coding-rules/init.js
server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/component-issues/init.js [deleted file]
server/sonar-web/src/main/js/apps/component-issues/routes.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js [deleted file]
server/sonar-web/src/main/js/apps/issues/HeaderView.js [deleted file]
server/sonar-web/src/main/js/apps/issues/component-viewer/main.js [deleted file]
server/sonar-web/src/main/js/apps/issues/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/AppContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/issues/components/IssuesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/ListItem.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/PageActions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/controller.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/author-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/base-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/context-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/file-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/language-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/module-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/project-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/status-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/facets/type-facet.js [deleted file]
server/sonar-web/src/main/js/apps/issues/init.js [deleted file]
server/sonar-web/src/main/js/apps/issues/issue-filter-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues/layout.js [deleted file]
server/sonar-web/src/main/js/apps/issues/models/issue.js [deleted file]
server/sonar-web/src/main/js/apps/issues/models/issues.js [deleted file]
server/sonar-web/src/main/js/apps/issues/models/state.js [deleted file]
server/sonar-web/src/main/js/apps/issues/redirects.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/router.js [deleted file]
server/sonar-web/src/main/js/apps/issues/routes.js
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/styles.css [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs [deleted file]
server/sonar-web/src/main/js/apps/issues/utils.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/workspace-header-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues/workspace-list-view.js [deleted file]
server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
server/sonar-web/src/main/js/apps/projects-admin/projects.js
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
server/sonar-web/src/main/js/apps/projects/components/App.js
server/sonar-web/src/main/js/apps/projects/components/NoProjects.js [deleted file]
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
server/sonar-web/src/main/js/apps/projects/styles.css
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
server/sonar-web/src/main/js/components/__tests__/issue-test.js [deleted file]
server/sonar-web/src/main/js/components/charts/bar-chart.js
server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
server/sonar-web/src/main/js/components/common/EmptySearch.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/MarkdownTips.js
server/sonar-web/src/main/js/components/common/SelectList.js
server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
server/sonar-web/src/main/js/components/common/action-options-view.js
server/sonar-web/src/main/js/components/common/modals.js
server/sonar-web/src/main/js/components/common/popup.js
server/sonar-web/src/main/js/components/controls/Checkbox.js
server/sonar-web/src/main/js/components/controls/DateInput.js
server/sonar-web/src/main/js/components/issue/BaseIssue.js [deleted file]
server/sonar-web/src/main/js/components/issue/ConnectedIssue.js [deleted file]
server/sonar-web/src/main/js/components/issue/Issue.js
server/sonar-web/src/main/js/components/issue/IssueView.js
server/sonar-web/src/main/js/components/issue/actions.js
server/sonar-web/src/main/js/components/issue/collections/issues.js [deleted file]
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
server/sonar-web/src/main/js/components/issue/components/IssueTags.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
server/sonar-web/src/main/js/components/issue/issue-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/models/changelog.js [deleted file]
server/sonar-web/src/main/js/components/issue/models/issue.js [deleted file]
server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/templates/issue.hbs [deleted file]
server/sonar-web/src/main/js/components/issue/types.js
server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/assign-form-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/changelog-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/comment-form-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/issue-popup.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/tags-form-view.js [deleted file]
server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js [deleted file]
server/sonar-web/src/main/js/components/layout/Page.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/layout/PageFilters.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/layout/PageMain.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/layout/PageMainInner.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/layout/PageSide.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
server/sonar-web/src/main/js/components/shared/Organization.js
server/sonar-web/src/main/js/components/shared/QualifierIcon.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/qualifier-icon.js [deleted file]
server/sonar-web/src/main/js/helpers/__tests__/issues-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js [deleted file]
server/sonar-web/src/main/js/helpers/issues.js
server/sonar-web/src/main/js/helpers/path.js
server/sonar-web/src/main/js/helpers/testUtils.js
server/sonar-web/src/main/js/helpers/urls.js
server/sonar-web/src/main/js/libs/third-party/keymaster.js [deleted file]
server/sonar-web/src/main/js/store/issues/duck.js [deleted file]
server/sonar-web/src/main/js/store/rootReducer.js
server/sonar-web/src/main/less/components/issues.less
server/sonar-web/src/main/less/components/page.less
server/sonar-web/src/main/less/components/react-select.less
server/sonar-web/src/main/less/components/search-navigator.less
server/sonar-web/src/main/less/pages/issues.less
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/perf/src/test/java/org/sonarsource/sonarqube/perf/server/WebTest.java
tests/upgrade/src/test/java/org/sonarsource/sonarqube/upgrade/UpgradeTest.java

index 94059da5a26ae9ff8922f9f71ac0060b51a06281..ee865958c232982fa4131e02438b2bb1c92b50d7 100644 (file)
@@ -156,14 +156,14 @@ public class IssueNotificationsTest extends AbstractIssueTest {
     assertThat((String) message.getContent()).contains("Severity");
     assertThat((String) message.getContent()).contains("One Issue Per Line (xoo): 17");
     assertThat((String) message.getContent()).contains(
-      "See it in SonarQube: http://localhost:9000/component_issues?id=sample#createdAt=2015-12-15T00%3A00%3A00%2B");
+      "See it in SonarQube: http://localhost:9000/project/issues?id=sample&createdAt=2015-12-15T00%3A00%3A00%2B");
 
     assertThat(emails.hasNext()).isTrue();
     message = emails.next().getMimeMessage();
     assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
     assertThat((String) message.getContent()).contains("sample/Sample.xoo");
     assertThat((String) message.getContent()).contains("Assignee changed to Tester");
-    assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key());
+    assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key());
 
     assertThat(emails.hasNext()).isFalse();
   }
@@ -218,7 +218,7 @@ public class IssueNotificationsTest extends AbstractIssueTest {
     assertThat((String) message.getContent()).contains("sample/Sample.xoo");
     assertThat((String) message.getContent()).contains("Severity: BLOCKER (was MINOR)");
     assertThat((String) message.getContent()).contains(
-      "See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key());
+      "See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key());
 
     assertThat(emails.hasNext()).isFalse();
   }
index de63813603323846048f2c969a93f40ca5cbf665..5c4007cf222544e271ded57d94d0fcd707dd531a 100644 (file)
@@ -28,7 +28,6 @@ import org.apache.commons.lang.time.DateUtils;
 import org.assertj.core.api.Fail;
 import org.junit.Before;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.sonar.wsclient.base.HttpException;
 import org.sonar.wsclient.base.Paging;
@@ -50,7 +49,6 @@ import static util.ItUtils.runProjectAnalysis;
 import static util.ItUtils.setServerProperty;
 import static util.ItUtils.toDate;
 import static util.ItUtils.verifyHttpException;
-import static util.selenium.Selenese.runSelenese;
 
 public class IssueSearchTest extends AbstractIssueTest {
 
@@ -273,17 +271,6 @@ public class IssueSearchTest extends AbstractIssueTest {
     assertThat(issues.component(issue).projectId()).isEqualTo(project.id());
   }
 
-  /**
-   * SONAR-5659
-   */
-  @Test
-  @Ignore("unstable")
-  public void redirect_to_search_url_after_wrong_login() {
-    // Force user authentication to check login on the issues search page
-    setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "true");
-    runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html");
-  }
-
   @Test
   public void return_issue_type() throws Exception {
     List<org.sonarqube.ws.Issues.Issue> issues = searchByRuleKey("xoo:OneBugIssuePerLine");
@@ -309,11 +296,6 @@ public class IssueSearchTest extends AbstractIssueTest {
     assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("VULNERABILITY"))).getPaging().getTotal()).isEqualTo(8);
   }
 
-  @Test
-  public void bulk_change() {
-    runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/bulk_change.html");
-  }
-
   private List<org.sonarqube.ws.Issues.Issue> searchByRuleKey(String... ruleKey) throws IOException {
     return searchIssues(new SearchWsRequest().setRules(asList(ruleKey))).getIssuesList();
   }
index 4c789853776f1ad0f3564122f3be63976470b439..b75534fd12f44e24f193e47d011d817b68933992 100644 (file)
@@ -87,14 +87,14 @@ public class UiTest {
     $(".overview-quality-gate")
       .shouldBe(visible)
       .shouldHave(text("Passed"));
-    $("a[href=\"/component_issues?id=sample#resolved=false|types=CODE_SMELL\"]")
+    $("a[href=\"/project/issues?id=sample&resolved=false&types=CODE_SMELL\"]")
       .shouldBe(visible)
       .shouldHave(text("0"))
       .click();
 
     // on project issues page
-    assertThat(url()).contains("/component_issues?id=sample#resolved=false|types=CODE_SMELL");
-    $(".facet.active[data-unresolved]").shouldBe(visible);
+    assertThat(url()).contains("/project/issues?id=sample&resolved=false&types=CODE_SMELL");
+    $("[data-property=\"resolutions\"] .facet.active").shouldBe(visible);
 
     $("#global-navigation").find("a[href=\"/profiles\"]").click();
 
index 801c24a9b0b35020fbd20483bb1e4d4ccbf76a8b..33972cc282342980be1fae2b0dd1ed88e0f6e733 100644 (file)
@@ -19,9 +19,7 @@
  */
 package pageobjects.issues;
 
-import com.codeborne.selenide.CollectionCondition;
 import com.codeborne.selenide.ElementsCollection;
-import com.codeborne.selenide.SelenideElement;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -55,23 +53,14 @@ public class IssuesPage {
 
   public IssuesPage bulkChangeOpen() {
     $("#issues-bulk-change").shouldBe(visible).click();
-    $("a.js-bulk-change").click();
     $("#bulk-change-form").shouldBe(visible);
     return this;
   }
 
   public IssuesPage bulkChangeAssigneeSearchCount(String query, Integer count) {
-    if (!$(".select2-drop-active").isDisplayed()) {
-      $("#bulk-change-form #s2id_assignee").shouldBe(visible).click();
-    }
-    SelenideElement input = $(".select2-drop-active input").shouldBe(visible);
-    input.val("").sendKeys(query);
-    if (count > 0) {
-      $(".select2-drop-active .select2-results li.select2-result").shouldBe(visible);
-    } else {
-      $(".select2-drop-active .select2-results li.select2-no-results").shouldBe(visible);
-    }
-    $$(".select2-drop-active .select2-results li.select2-result").shouldHaveSize(count);
+    $("#issues-bulk-change-assignee .Select-input input").val(query);
+    $$("#issues-bulk-change-assignee .Select-option").shouldHaveSize(count);
+    $("#issues-bulk-change-assignee .Select-input input").pressEscape();
     return this;
   }
 }
diff --git a/it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html b/it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html
deleted file mode 100644 (file)
index c390c02..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-<tbody>
-<tr>
-       <td>open</td>
-       <td>/sessions/logout</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/sessions/new</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>content</td>
-       <td>*Log In to SonarQube*</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>id=login</td>
-       <td>admin</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>id=password</td>
-       <td>admin</td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>commit</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=.js-user-authenticated</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/issues</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=.search-navigator-workspace-list .issue</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=issues-bulk-change</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>id=issues-bulk-change</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=bulk-change-form</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=transition-confirm</td>
-       <td></td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
diff --git a/it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html b/it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html
deleted file mode 100644 (file)
index 791967e..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/logout</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>open</td>
-    <td>/issues#resolved=true|statuses=OPEN</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log In to SonarQube*</td>
-  </tr>
-  <tr>
-    <td>waitForElementPresent</td>
-    <td>id=login</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=password</td>
-    <td>wrongpassword</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=login</td>
-    <td>wronglogin</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=password</td>
-    <td>wrongpassword</td>
-  </tr>
-  <tr>
-    <td>clickAndWait</td>
-    <td>commit</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>css=.alert</td>
-    <td>*Authentication failed*</td>
-  </tr>
-  <tr>
-    <td>waitForElementPresent</td>
-    <td>id=login</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=login</td>
-    <td>admin</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>id=password</td>
-    <td>admin</td>
-  </tr>
-  <tr>
-    <td>clickAndWait</td>
-    <td>commit</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>assertLocation</td>
-    <td>*#resolved=true|statuses=OPEN*</td>
-    <td></td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
index 490a57d751409aa9867cb197332f213928588bff..09c6611dcfe72d098362f97949b095cf1b7f4916 100644 (file)
@@ -175,7 +175,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
     String dateString = notification.getFieldValue(FIELD_PROJECT_DATE);
     if (projectKey != null && dateString != null) {
       Date date = DateUtils.parseDateTime(dateString);
-      String url = String.format("%s/component_issues?id=%s#createdAt=%s",
+      String url = String.format("%s/project/issues?id=%s&createdAt=%s",
         settings.getServerBaseURL(), encode(projectKey), encode(DateUtils.formatDateTime(date)));
       message
         .append("See it in SonarQube: ")
index bcf9a93317f823d74ba6bed156762fb1df1002b5..348886958b741ee9690434a620c3a81e37d7c33f 100644 (file)
@@ -102,7 +102,7 @@ public class IssueChangesEmailTemplate extends EmailTemplate {
 
   private void appendFooter(StringBuilder sb, Notification notification) {
     String issueKey = notification.getFieldValue("key");
-    sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues/search#issues=").append(issueKey).append(NEW_LINE);
+    sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues?issues=").append(issueKey).append(NEW_LINE);
   }
 
   private static void appendLine(StringBuilder sb, @Nullable String line) {
index 756d4b1a67b7e51483a7085b0af50fa1f81101d2..e57c8f4beb1ebcfcf2d41f92752ccf2f2b93de2f 100644 (file)
@@ -60,7 +60,7 @@ public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate {
     String assignee = notification.getFieldValue(FIELD_ASSIGNEE);
     if (projectUuid != null && dateString != null && assignee != null) {
       Date date = DateUtils.parseDateTime(dateString);
-      String url = String.format("%s/issues/search#projectUuids=%s|assignees=%s|createdAt=%s",
+      String url = String.format("%s/issues?projectUuids=%s&assignees=%s&createdAt=%s",
         settings.getServerBaseURL(),
         encode(projectUuid),
         encode(assignee),
index 48de7c3858568469a7d3359ddf4964553d8c640f..b4497b7fbe80cfefdb62af96e85c424ccd60439e 100644 (file)
@@ -9,4 +9,4 @@ Resolution: FALSE-POSITIVE
 Status: RESOLVED
 Tags: [bug performance]
 
-See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
+See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE
index c2e53d7a50f96b2dc3d40965aae00d335c7d33c5..5b020cfc5e80e98dba674195444765a3ee6bd553 100644 (file)
@@ -17,4 +17,4 @@ Project: Struts
         /path/to/file: 3
         /path/to/directory: 7
 
-See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
\ No newline at end of file
+See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1
\ No newline at end of file
index a502a4075676c9d39b647b2e02e0f01359f7541c..7d978bdfcaac35f8e3da836f4286bf44a1ee9823 100644 (file)
@@ -5,4 +5,4 @@ Project: Struts
     Severity
         Blocker: 0    Critical: 5    Major: 10    Minor: 3    Info: 1
 
-See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
\ No newline at end of file
+See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1
\ No newline at end of file
index 38a8cd6bf68ad095102100a2cc84a965c80abdf8..7c4bb6c0118a45d28b0f649e8f9dbe8ed577ded2 100644 (file)
@@ -21,4 +21,4 @@ Project: Struts
         /path/to/file: 3
         /path/to/directory: 7
 
-See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1
\ No newline at end of file
+See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1
\ No newline at end of file
index 627de81c7b2b98ba040a754ad4f9bec71da8e666..6c6e92ee3ce60c647b0d82d3748eb62b3b129d61 100644 (file)
@@ -5,4 +5,4 @@ Project: Struts
     Severity
         Blocker: 0    Critical: 5    Major: 10    Minor: 3    Info: 1
 
-See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1
\ No newline at end of file
+See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1
\ No newline at end of file
index d6e85fb913937a40f2f7da7f9cac5e93104c10e3..6b892cf526f394ab0aa9fe01c4b96b7b6d000929 100644 (file)
@@ -11,7 +11,6 @@
   },
 
   "globals": {
-    "key": true,
     "baseUrl": true,
     "SyntheticInputEvent": true
   },
index 0de9ca396318764c0bf99d3b5ca36a7bee23cfd0..e8b615bccf850aeca0f899e017415b0b35036032 100644 (file)
@@ -25,7 +25,6 @@ module.exports = {
       'handlebars/runtime',
       './src/main/js/libs/third-party/jquery-ui.js',
       './src/main/js/libs/third-party/select2.js',
-      './src/main/js/libs/third-party/keymaster.js',
       './src/main/js/libs/third-party/bootstrap/tooltip.js',
       './src/main/js/libs/third-party/bootstrap/dropdown.js'
     ],
@@ -92,7 +91,7 @@ module.exports = {
       { test: require.resolve('react-dom'), loader: 'expose?ReactDOM' }
     ]
   },
-  postcss: function() {
+  postcss() {
     return [autoprefixer(autoprefixerOptions)];
   },
   // Some libraries import Node modules but don't use them in the browser.
index 3defe39abc689f2b849437508389a97c2b50953c..efa2cf9ad24e6164da888151ae9c480ea8cf0823 100644 (file)
     "d3-selection": "1.0.5",
     "d3-shape": "1.0.6",
     "escape-html": "1.0.3",
+    "glamor": "2.20.24",
     "handlebars": "2.0.0",
     "history": "2.0.0",
     "jquery": "2.2.0",
+    "keymaster": "1.6.2",
     "lodash": "4.6.1",
     "moment": "2.10.6",
     "numeral": "1.5.3",
index 512f3a1f7ef3032dcdaf0a06f3090257cd116f85..c9bea5e039daf67719473633014557fc0a8ad2a7 100644 (file)
@@ -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
index 912f8b2f6d35d2215196b61dba56d9980d5f2557..ae316d8c228ef2ab55bdbc50ae9a54395a2bc7b7 100644 (file)
@@ -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);
index 2f668b0b8b93e47d2ef2e073651b375242d73893..0d4200d6bf5928ba7ad68d82cc04a526febe2b85 100644 (file)
@@ -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';
 
index 6b5ba3661a8af9665c38c07bbb73609138ba29a0..6c68203c03b2bb9bda2606063e0d033b5ca5fc74 100644 (file)
@@ -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()}
index d9bd895c7b1a75c14011a8a6aa4a4a1b5ee8a6d5..810d438e08e5f285197970a507d98f07bb7ae60e 100644 (file)
@@ -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
index d6af75c3865a181739a25429f749ff9467d285b6..898c8de040f6543f7e1d37b5aace01605ab53b06 100644 (file)
@@ -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",
           },
         }
       }>
index 1c7f395f74147d1394af18759d360dcaeaf0e532..3a5f67cdadabbe3b34e5d6e876b862638d46adc0 100644 (file)
@@ -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>
index cb8349ecca58257ca171acd102ba1855358a9363..d7089b5a536fa915335f5d5ea26bcf80f260214d 100644 (file)
@@ -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';
 
index 163ec964e26912f15a4c4fb1ec0c2b6e380278c7..9277cb5acb5ab3332f5485d44734b846e8d45dd0 100644 (file)
@@ -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'
index d21f3f609ee597bb69c04c96d8a529ea7dcfb32c..99b97c82c4f6816433c34e3440f28e87eb5e96f5 100644 (file)
@@ -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();
 });
index 2f2dc6dd0f3c304f95df50fcacb64bc6881307ba..30ec923b76afdde4ef0c1ae649697c78ccb96a9e 100644 (file)
@@ -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>
index 34f86b21085709df33d4f20bd9f0dbb714333034..932192fabc32b0ed5e46ff5498e060626651ffe4 100644 (file)
@@ -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>
index 3ae7d82992da106fd24fa5fed80db936560559c3..fd231aa512bcaf55454bec1bffe8590264d9910e 100644 (file)
@@ -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>
index 3b974ffd7703f4d4ab3eab274e97bbc928f80e4f..11db35cac3b1e82c5d3988c448b885ae3f3a06ad 100644 (file)
@@ -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>
index 6c7d13c15b7702256442cc4e18aee0af799e91a2..2a9fb4157ae8203d4b01417f5bd05e7d4ac85032 100644 (file)
@@ -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';
 
index 9ad27ba80ecdb46734cc2cf5099eb4685a9edbf1..76ea80e652fc9b6ef75cd33c00f2ad5cead15a1f 100644 (file)
@@ -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';
index 01943b685bfd3808c8b7076056e4873ac4307f61..dd15fb39b92dd924e303fca2ba7aaff79e00e588 100644 (file)
@@ -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';
index 57e689c51788028b13904b45283331b99215f6b0..437505c5eaf7d791af689a2674227650cba1aa5b 100644 (file)
@@ -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
       });
index 307765e54c6609705cb545d48f0129f52565108b..722e3f22d3ec31f35e3627e77ca2bcf8b67adc6f 100644 (file)
@@ -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';
index 1e32a633a9a5e2f097f944315ee71bcc2ea3d93a..b218d6939b90e1d6786dcbccf355a4671ab94e55 100644 (file)
@@ -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/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js
deleted file mode 100644 (file)
index 061b3e8..0000000
+++ /dev/null
@@ -1,52 +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 React from 'react';
-import { connect } from 'react-redux';
-import init from '../init';
-import { getComponent, getCurrentUser } from '../../../store/rootReducer';
-
-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>
-    );
-  }
-}
-
-const mapStateToProps = (state, ownProps) => ({
-  component: getComponent(state, ownProps.location.query.id),
-  currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(ComponentIssuesAppContainer);
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 (file)
index 4b5abd4..0000000
+++ /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-issues/routes.js b/server/sonar-web/src/main/js/apps/component-issues/routes.js
deleted file mode 100644 (file)
index 954ba14..0000000
+++ /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.
- */
-const routes = [
-  {
-    indexRoute: {
-      getComponent(_, callback) {
-        require.ensure([], require =>
-          callback(null, require('./components/ComponentIssuesAppContainer').default));
-      }
-    }
-  }
-];
-
-export default routes;
index 03d857462bf2736637e9735673aaf989c104d854..5261badeaf2925103235fc6b9b8c887e10bdd804 100644 (file)
@@ -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';
 
index 1ccdd7eed1b39456a811ab5343d782f0c381a5bf..43d0d8bd422188209e06a533bee936a4109a8817 100644 (file)
@@ -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 (file)
index 1c9b447..0000000
+++ /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 (file)
index 596996f..0000000
+++ /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 (file)
index a1dcb64..0000000
+++ /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 (file)
index 0000000..73574c6
--- /dev/null
@@ -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/issues/components/AppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
new file mode 100644 (file)
index 0000000..c605961
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 { connect } from 'react-redux';
+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';
+
+type Query = { [string]: string };
+
+const mapStateToProps = (state, ownProps) => ({
+  component: ownProps.location.query.id
+    ? getComponent(state, ownProps.location.query.id)
+    : undefined,
+  currentUser: getCurrentUser(state)
+});
+
+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 (file)
index 0000000..3b7d454
--- /dev/null
@@ -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 (file)
index 0000000..bc12da4
--- /dev/null
@@ -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/FiltersHeader.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
new file mode 100644 (file)
index 0000000..9740f63
--- /dev/null
@@ -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 React from 'react';
+import { css } from 'glamor';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  displayReset: boolean,
+  onReset: () => void
+};
+
+const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });
+
+export default class FiltersHeader extends React.PureComponent {
+  props: Props;
+
+  handleResetClick = (e: Event & { currentTarget: HTMLElement }) => {
+    e.preventDefault();
+    e.currentTarget.blur();
+    this.props.onReset();
+  };
+
+  render() {
+    return (
+      <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>
+    );
+  }
+}
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 (file)
index 0000000..2d8530d
--- /dev/null
@@ -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/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js
deleted file mode 100644 (file)
index b1f3e70..0000000
+++ /dev/null
@@ -1,55 +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 React from 'react';
-import { connect } from 'react-redux';
-import init from '../init';
-import { getCurrentUser } from '../../../store/rootReducer';
-
-class IssuesAppContainer extends React.Component {
-  static propTypes = {
-    currentUser: React.PropTypes.any.isRequired
-  };
-
-  componentDidMount() {
-    this.stop = init(this.refs.container, 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>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(IssuesAppContainer);
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 (file)
index 0000000..4bd9cf9
--- /dev/null
@@ -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 (file)
index 0000000..ff090ed
--- /dev/null
@@ -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 (file)
index 0000000..f8a0bdd
--- /dev/null
@@ -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 (file)
index 0000000..a8f6441
--- /dev/null
@@ -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 (file)
index 0000000..262c3ad
--- /dev/null
@@ -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 (file)
index 0000000..eddea69
--- /dev/null
@@ -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 (file)
index 0000000..f4d46d4
--- /dev/null
@@ -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 (file)
index 0000000..4f2fe37
--- /dev/null
@@ -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 (file)
index f98f292..0000000
+++ /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 (file)
index f706ef5..0000000
+++ /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 (file)
index 597afca..0000000
+++ /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 (file)
index c652eba..0000000
+++ /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 (file)
index 5d05caa..0000000
+++ /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 (file)
index a2f31cb..0000000
+++ /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 (file)
index 81c3197..0000000
+++ /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 (file)
index a6168f3..0000000
+++ /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 (file)
index 11b9a69..0000000
+++ /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 (file)
index d57d9c7..0000000
+++ /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 (file)
index 789ae16..0000000
+++ /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/mode-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js
deleted file mode 100644 (file)
index dd54e82..0000000
+++ /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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-mode-facet.hbs';
-
-export default BaseFacet.extend({
-  template: Template,
-
-  toggleFacet(e) {
-    const isCount = $(e.currentTarget).is('[data-value="count"]');
-    return this.options.app.state.updateFilter({
-      facetMode: isCount ? 'count' : 'effort'
-    });
-  },
-
-  serializeData() {
-    return {
-      ...BaseFacet.prototype.serializeData.apply(this, arguments),
-      mode: this.options.app.state.getFacetMode()
-    };
-  }
-});
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 (file)
index b6e8759..0000000
+++ /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 (file)
index 9bd37e6..0000000
+++ /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 (file)
index 3e691ae..0000000
+++ /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 (file)
index d4e01b1..0000000
+++ /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 (file)
index 7544557..0000000
+++ /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 (file)
index db5cd33..0000000
+++ /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 (file)
index cb4e88a..0000000
+++ /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 (file)
index 8bb629f..0000000
+++ /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 (file)
index efac17e..0000000
+++ /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 (file)
index 0fb32e8..0000000
+++ /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 (file)
index c3f45ef..0000000
+++ /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 (file)
index c796bac..0000000
+++ /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 (file)
index 218e278..0000000
+++ /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 (file)
index 9ea6454..0000000
+++ /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 (file)
index b367cc6..0000000
+++ /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 (file)
index 0000000..3efc1af
--- /dev/null
@@ -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 (file)
index ac35322..0000000
+++ /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));
-  }
-});
index 5f3a27c905c69fac666111e815514d58234b4764..61893ce7fec2945fac93b813f6a033cc08a19d65 100644 (file)
  * 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 (file)
index 0000000..cf7bb4b
--- /dev/null
@@ -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 (file)
index 0000000..d539229
--- /dev/null
@@ -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 (file)
index 0000000..dd28ccf
--- /dev/null
@@ -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 (file)
index 0000000..ddd82db
--- /dev/null
@@ -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 (file)
index 0000000..dcfb162
--- /dev/null
@@ -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 (file)
index 0000000..5d914f8
--- /dev/null
@@ -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 (file)
index 0000000..411c9b7
--- /dev/null
@@ -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 (file)
index 0000000..4ad3bc3
--- /dev/null
@@ -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 (file)
index 0000000..8711e01
--- /dev/null
@@ -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 (file)
index 0000000..2b7046f
--- /dev/null
@@ -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 (file)
index 0000000..d83c56c
--- /dev/null
@@ -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 (file)
index 0000000..ce5d5d4
--- /dev/null
@@ -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/issues/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
new file mode 100644 (file)
index 0000000..e95f440
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 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,
+  severities: Array<string>,
+  stats?: { [string]: number }
+|};
+
+export default class SeverityFacet extends React.PureComponent {
+  props: Props;
+
+  static defaultProps = {
+    open: true
+  };
+
+  property = 'severities';
+
+  handleItemClick = (itemValue: string) => {
+    const { severities } = this.props;
+    const newValue = orderBy(
+      severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
+    );
+    this.props.onChange({ [this.property]: newValue });
+  };
+
+  handleHeaderClick = () => {
+    this.props.onToggle(this.property);
+  };
+
+  getStat(severity: string): ?number {
+    const { stats } = this.props;
+    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'];
+
+    return (
+      <FacetBox property={this.property}>
+        <FacetHeader
+          hasValue={this.props.severities.length > 0}
+          name={translate('issues.facet', this.property)}
+          onClick={this.handleHeaderClick}
+          open={this.props.open}
+        />
+
+        {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 (file)
index 0000000..fd4c2c3
--- /dev/null
@@ -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 (file)
index 0000000..40bfb25
--- /dev/null
@@ -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 (file)
index 0000000..cd6ab46
--- /dev/null
@@ -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 (file)
index 0000000..c0eb027
--- /dev/null
@@ -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 (file)
index 0000000..5dc2230
--- /dev/null
@@ -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 (file)
index 0000000..a55c903
--- /dev/null
@@ -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 (file)
index 0000000..80726a2
--- /dev/null
@@ -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 (file)
index 0000000..81d6ce8
--- /dev/null
@@ -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 (file)
index 0000000..358f6ee
--- /dev/null
@@ -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/apps/issues/sidebar/components/FacetFooter.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js
new file mode 100644 (file)
index 0000000..8f0fca2
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 SearchSelect from '../../components/SearchSelect';
+
+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<*>
+|};
+
+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 (file)
index 0000000..ff6ef83
--- /dev/null
@@ -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 (file)
index 0000000..94d512d
--- /dev/null
@@ -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/issues/sidebar/components/FacetItemsList.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js
new file mode 100644 (file)
index 0000000..4a203f0
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+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/apps/issues/sidebar/components/__tests__/FacetBox-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js
new file mode 100644 (file)
index 0000000..0eb453e
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 FacetBox from '../FacetBox';
+
+it('should render', () => {
+  expect(shallow(<FacetBox property="foo"><div /></FacetBox>)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js
new file mode 100644 (file)
index 0000000..4dbf1cc
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 FacetFooter from '../FacetFooter';
+
+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 (file)
index 0000000..5aa00c4
--- /dev/null
@@ -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 (file)
index 0000000..ddc84ec
--- /dev/null
@@ -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/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js
new file mode 100644 (file)
index 0000000..883e62b
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 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 (file)
index 0000000..2722912
--- /dev/null
@@ -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 (file)
index 0000000..21f8096
--- /dev/null
@@ -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 (file)
index 0000000..3333ae8
--- /dev/null
@@ -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 (file)
index 0000000..0a22b71
--- /dev/null
@@ -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 (file)
index 0000000..15686ac
--- /dev/null
@@ -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 (file)
index 46942bb..0000000
+++ /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 (file)
index fda291a..0000000
+++ /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 (file)
index 64237bc..0000000
+++ /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 (file)
index 9fb23f7..0000000
+++ /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 (file)
index 83442f8..0000000
+++ /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 (file)
index 9f981c0..0000000
+++ /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 (file)
index bf54108..0000000
+++ /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 (file)
index 0674f5b..0000000
+++ /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 (file)
index 3569040..0000000
+++ /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 (file)
index 28ae140..0000000
+++ /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 (file)
index af69286..0000000
+++ /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 (file)
index c11181a..0000000
+++ /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 (file)
index 7693af6..0000000
+++ /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 (file)
index 19e4707..0000000
+++ /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 (file)
index 88b9bd0..0000000
+++ /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 (file)
index cc7f1fc..0000000
+++ /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 (file)
index f97ada4..0000000
+++ /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 (file)
index f18f684..0000000
+++ /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 (file)
index 83add61..0000000
+++ /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 (file)
index 6a28b92..0000000
+++ /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 (file)
index b0e305e..0000000
+++ /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 (file)
index cae9c96..0000000
+++ /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 (file)
index 0000000..3c9d8a2
--- /dev/null
@@ -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 (file)
index 0fccbb3..0000000
+++ /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-empty-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js
deleted file mode 100644 (file)
index eea72dc..0000000
+++ /dev/null
@@ -1,29 +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 { translate } from '../../helpers/l10n';
-
-export default Marionette.ItemView.extend({
-  className: 'search-navigator-no-results',
-
-  template() {
-    return translate('issue_filter.no_issues');
-  }
-});
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 (file)
index ec59af5..0000000
+++ /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 (file)
index f74b7c9..0000000
+++ /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/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
deleted file mode 100644 (file)
index cadec29..0000000
+++ /dev/null
@@ -1,94 +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 { orderBy, 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 SeverityHelper from '../../../components/shared/SeverityHelper';
-import { translate } from '../../../helpers/l10n';
-
-type Props = {|
-  onChange: (changes: { [string]: Array<string> }) => void,
-  onToggle: (property: string) => void,
-  open: boolean,
-  severities: Array<string>,
-  stats?: { [string]: number }
-|};
-
-export default class SeverityFacet extends React.PureComponent {
-  props: Props;
-
-  static defaultProps = {
-    open: true
-  };
-
-  property = 'severities';
-
-  handleItemClick = (itemValue: string) => {
-    const { severities } = this.props;
-    const newValue = orderBy(
-      severities.includes(itemValue)
-        ? without(severities, itemValue)
-        : uniq([...severities, itemValue])
-    );
-    this.props.onChange({ [this.property]: newValue });
-  };
-
-  handleHeaderClick = () => {
-    this.props.onToggle(this.property);
-  };
-
-  getStat(severity: string): ?number {
-    const { stats } = this.props;
-    return stats ? stats[severity] : null;
-  }
-
-  render() {
-    const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
-
-    return (
-      <FacetBox property={this.property}>
-        <FacetHeader
-          hasValue={this.props.severities.length > 0}
-          name={translate('issues.facet', this.property)}
-          onClick={this.handleHeaderClick}
-          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>
-      </FacetBox>
-    );
-  }
-}
index bf1b7d89862f43dd5064886f9d80ba3b15790ec3..1fba6f721196606dfa76d5183d0f25dd40a728bc 100644 (file)
@@ -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}
index 77eabf062dae4a9e37ec406fa30211391badc8a0..67dbdf8bcd4ae157c064cced0d6fc2ff1a4a554b 100644 (file)
@@ -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}
index d04badd1fafa7d8348a4cc05a45b3c5d3890c9e6..b29c17b3a5c4d68777832649227586252468ebfa 100644 (file)
@@ -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
index 2d97ab9b99670f05857b6e7a738b8803eb3b66f3..9fd4ffc417e3b06039c9a574b92fc20efe4feef9 100644 (file)
@@ -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';
index 7928cb05f4c90319d0e762058475321dc2d3c478..bb722731c4fe5576c31fde7be6e966c7d21f41bf 100644 (file)
@@ -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() {
index 88c0940368da0a079409aa89bc7bd996e01fad74..f3984b0cbdddfd344cb3e14a757a8e4d33a65fde 100644 (file)
@@ -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 {
index ec5299b31a8e9cc323a9608b1dab05a3282d7e63..c62d9e8c612d56519b8ba7843154d432cc4abc35 100644 (file)
@@ -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>
     );
   }
 }
index c5c3b94bda17533e474f5df29c227a6df46235f9..69c18380bb3d710ba6b51821101f11a86984ef71 100644 (file)
@@ -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/NoProjects.js b/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js
deleted file mode 100644 (file)
index c2194ad..0000000
+++ /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 React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-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>
-    );
-  }
-}
index ed924559a8554de3a4e9a4cff022db5afb7facd3..d102a9dcef4876a957a5011f454b09ee1ee355ee 100644 (file)
@@ -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>}
 
index 1c4cae090f2a5b8d4415b11ac06443a36c886584..d692e2ca9cee74989b6305c03ac44173f549d2ea 100644 (file)
@@ -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 />;
     }
   }
 
index 211b0fbafb12b8f4125211499845a4afa8ced77d..faf96ac7d0a2e070dfba151bece75635539a8dc6 100644 (file)
@@ -16,7 +16,7 @@ exports[`test should handle \`view\` and \`visualization\` 2`] = `
         },
       }
     }>
-    projects.clear_all_filters
+    clear_all_filters
   </Link>
 </div>
 `;
index 45f8cd2663baaf0c1711575964a0a8be32781f30..99e3232f7d02fb432aa5d24ee19ba1e7dc0e2291 100644 (file)
   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;
index 6d877d785fa0a190339bb23275b1d6f8f5f24687..171d4ab8e0d508cec343baf3b2506dbebd8994da 100644 (file)
@@ -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';
index 0acd2dc123dcbbcf0fe36fc142c0a17dbacb04a8..41517e76d50404b30ff53201092e34efd590adc1 100644 (file)
@@ -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);
index c0cae1208f816047290a5e3e813d5d35a517fa04..2e66388c3a1a8dae7df7b8a58ba9a1e8a08a0aab 100644 (file)
@@ -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}
index 8b9cfb46bd56f9508fa4a4783d9b48e7f2076d4f..64aeedd5ba6c749dcb8b3feee640624edbb71666 100644 (file)
@@ -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}
index 4b65cd32edea4738466a379ca1e52be5738caafa..523ceeb192f11fa313e49d13a6c0624eec6be836 100644 (file)
@@ -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}
index 74f3dabbfaea9e74a742e1783abbc822285e0c8f..b1d051389a53e3ff1aa34d67034c57fe84ff6e6c 100644 (file)
@@ -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}
index b5813f763651fcfb66206d3bb2073ad00b944912..6b18e06791b961117a7f00652dbc80c911819ab4 100644 (file)
@@ -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}
           />}
index daf1785ffd2d40c2ddeb998d74ad07c39a6dd987..b7f1c2a176e295e7c750a7d5b8bf53aa14db1975 100644 (file)
@@ -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/LineIssuesIndicatorContainer.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
deleted file mode 100644 (file)
index 8d15af0..0000000
+++ /dev/null
@@ -1,29 +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 { connect } from 'react-redux';
-import LineIssuesIndicator from './LineIssuesIndicator';
-import { getIssueByKey } from '../../../store/rootReducer';
-
-const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({
-  issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey))
-});
-
-export default connect(mapStateToProps)(LineIssuesIndicator);
index ca89ab51fae978b4e291cea39142ac1c37c3d3d4..bff245af97cd6ba6fa99d4900fff6a9c53dd2c85 100644 (file)
  */
 // @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>
index 3cc6793b214d8551730b45739737df4e5b4619d0..5cd841a2a5a98f89f9961f69ef4cb948dcb81bcf 100644 (file)
@@ -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()}
index ede9d50241c4cdccde04919832baf0ebe355f8e9..a9a0e0763b91f408ed5ec603cc92e8ca2f96bef3 100644 (file)
@@ -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();
 });
index 3e4499bb1bf658880fdc80f5a47e54e5af9d3f65..ca94ecedbf709ab9cf23dc73e6d8e66f07ddac04 100644 (file)
@@ -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]}
index 30bbfaa7779b9d1eee7373f5b84fff2658428356..090d0852df8fc409117b9320b243ca733dae4a8b 100644 (file)
@@ -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>
index 36bf7e73b3a8723de2dcf38e57e6dedd0411c4c5..d39351c52dc78f14889aeaf99775565ad3c73cb7 100644 (file)
@@ -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 (file)
index b0483b8..0000000
+++ /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);
-    });
-  });
-});
index dc4f0082f27d9b94b083d7b505c65ef35a26c7c8..4e1d336dc0d7a7ebaed2df3a2fe22ca28f11e832 100644 (file)
@@ -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>
     );
   }
 });
index d91207af777bf879efcc1880c8b6534d740ed01b..7b06317b39e05ced6defd1ea1879397bb23ce306 100644 (file)
@@ -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 (file)
index 0000000..904a6b2
--- /dev/null
@@ -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;
index 2d83b6aeb24521f80eebdfbb4dd82a1cc34b2b2d..8c5db3a8fe12ebd7807706b9a7e924889c628c73 100644 (file)
@@ -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() {
index ba2f82b34b756ed71d6d6eff1fe3e8f73c0cf855..bec5c2e6712ccdcf017f68e5618621b3f3ab0a20 100644 (file)
@@ -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
index 9c0e88e6aa37669a47a9dde408c48a23eee2aa38..58afbecad26916e673cb3615243574f4c0183080 100644 (file)
@@ -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
index 4cf15f469cb8692cf49054f94155518231faec17..b2d9388c7ef4a8aa71e0d90cdf538c0560076dca 100644 (file)
@@ -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"
index 976f88eb081d8ee2eb18695007027a79e475597e..a538e22b88eca5ff5a10a9cc551d559de765db34 100644 (file)
@@ -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({
index 5a1343bd511290223ffbf418dd53d13de0aaf5fc..a86a262d40c31eaec78cab6746ff429a0c4de4b8 100644 (file)
@@ -19,6 +19,7 @@
  */
 import $ from 'jquery';
 import Marionette from 'backbone.marionette';
+import key from 'keymaster';
 
 const EVENT_SCOPE = 'modal';
 
index 532380f71b40f787e5f44840a104000c1939058d..bce61b72938520ceb681e42b9ecb5204593258b4 100644 (file)
@@ -19,6 +19,7 @@
  */
 import $ from 'jquery';
 import Marionette from 'backbone.marionette';
+import key from 'keymaster';
 
 export default Marionette.ItemView.extend({
   className: 'bubble-popup',
index f5e7289dd45fb66d0129d2e9556fdbe126c261c9..c81d49b8d8c176959ad2f4ab8671c2caa2d22ec5 100644 (file)
@@ -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
     });
index 52b47cbe18969ac01f5fa248e6ab2a8a0a6663c2..f4070c12119c5a4e746a2162f0c25533024b3600 100644 (file)
  */
 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 (file)
index d4ada02..0000000
+++ /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/ConnectedIssue.js b/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
deleted file mode 100644 (file)
index 67d71fe..0000000
+++ /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.
- */
-// @flow
-import { connect } from 'react-redux';
-import BaseIssue from './BaseIssue';
-import { getIssueByKey } from '../../store/rootReducer';
-import { onFail } from '../../store/rootActions';
-import { updateIssue } from './actions';
-
-const mapStateToProps = (state, ownProps) => ({
-  issue: getIssueByKey(state, ownProps.issueKey)
-});
-
-const mapDispatchToProps = {
-  onIssueChange: updateIssue,
-  onFail: error => dispatch => onFail(dispatch)(error)
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(BaseIssue);
index a121bf738d00904fab2a44e4a151fabaf5a8513e..471a62e04f0f8ff32f8c99ad649f6ae17199cd60 100644 (file)
  * 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}
+      />
+    );
+  }
+}
index 52ee7e95280318c8e193ef9c2bf5010166174fce..3d959f265736a289d5bc09322b00c7f1c5a87f3f 100644 (file)
 // @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>
     );
   }
index a0631c17001631e675b59e76808b708068cdcd5b..a44430520bd49180b2983fb982c88b26783d6f60 100644 (file)
  * 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 (file)
index 69ac37b..0000000
+++ /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;
-    });
-  }
-});
index e60bc87c9911daf662d6ff61f7b68c609650ebb3..9006cd9a19e271cdc4d0d8baf233790681c9c88d 100644 (file)
@@ -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>
index f5f6bf5b8d32e55be7be3f144a538dd4ed52fd87..8111815e942be27f35966697e8fd37b2514f4086 100644 (file)
  */
 // @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);
   };
 
index ab850061b7dce7775f13b9978607690c5d6bcf7a..c7cebdf74d7b9ea0af5275c32f72ce046edb12dd 100644 (file)
@@ -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
index 4f847049f54181e6467d08587b95162794a75932..55ef295f55d195247c25bfc22aa692993ede1d91 100644 (file)
  */
 // @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>
index 03cd4e41d86b503fa82029dc039cbbe4037f7bed..24e3625d5296cac886f3e73fab7fc42644af9b7a 100644 (file)
@@ -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 (file)
index 0000000..c28593d
--- /dev/null
@@ -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>
+    );
+  }
+}
index ca4a95ff08b76c3884463aeab5d31425e5b16d65..608112423d593ffe00985cf6db1bebf8c45d96e3 100644 (file)
@@ -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(
index d681183f2c3f07b31a140d9cbbbf58a4a1807472..9096b72938697989a3ec1a937e553f6899043258 100644 (file)
@@ -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(
index 3e110b92f3633f1907eeb17d9c17b0f47f797ef7..1d3b7ac4e0e9c42236697586cda3a386fcd2a85c 100644 (file)
@@ -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()}
     />
   );
index f51811bbd0f2a57b9a7ce6dd59b7841fad9b4c9f..e00752935ca3145b574fe359ae6260b73c1ef4b6 100644 (file)
@@ -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 (file)
index e9b4c47..0000000
+++ /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/changelog.js b/server/sonar-web/src/main/js/components/issue/models/changelog.js
deleted file mode 100644 (file)
index bf1150a..0000000
+++ /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 Backbone from 'backbone';
-
-export default Backbone.Collection.extend({
-  url() {
-    return window.baseUrl + '/api/issues/changelog';
-  },
-
-  parse(r) {
-    return r.changelog;
-  }
-});
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 (file)
index 1abeee0..0000000
+++ /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 (file)
index 0000000..88d352e
--- /dev/null
@@ -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>
+    );
+  }
+}
index 6c4f9d5977eed1c5d977adfb3ddaa32d081b0316..35d5c05b5f2228a98f93efb37bcef991dc72f354 100644 (file)
@@ -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 (file)
index 939bf52..0000000
+++ /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 (file)
index d88b8a7..0000000
+++ /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 (file)
index 29550cd..0000000
+++ /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 (file)
index 64d2d0d..0000000
+++ /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 (file)
index 7ba2e7c..0000000
+++ /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 (file)
index 9184dd3..0000000
+++ /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 (file)
index ea6a3a9..0000000
+++ /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 (file)
index 3f42921..0000000
+++ /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 (file)
index 90df7aa..0000000
+++ /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 (file)
index 64d2d0d..0000000
+++ /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 (file)
index ef8ae2f..0000000
+++ /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 (file)
index 72e59a5..0000000
+++ /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}}
index 690c38146cb8c96cdb2ec62cd1ae09ef1f410322..4a07b129eeb8dd1f8f7c3652c40043e17f4b6fb5 100644 (file)
@@ -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 (file)
index 3360de2..0000000
+++ /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 (file)
index a3e81ef..0000000
+++ /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 (file)
index b04b56a..0000000
+++ /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 (file)
index 52d68bc..0000000
+++ /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 (file)
index 96488cd..0000000
+++ /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 (file)
index b30c689..0000000
+++ /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 (file)
index 719d679..0000000
+++ /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 (file)
index 87b1287..0000000
+++ /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 (file)
index 0a44b5a..0000000
+++ /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/layout/Page.js b/server/sonar-web/src/main/js/components/layout/Page.js
new file mode 100644 (file)
index 0000000..a8adef5
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 = {
+  className?: string,
+  children?: React.Element<*>
+};
+
+const styles = css({
+  display: 'flex',
+  alignItems: 'stretch',
+  width: '100%',
+  flexGrow: 1
+});
+
+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/components/layout/PageFilters.js b/server/sonar-web/src/main/js/components/layout/PageFilters.js
new file mode 100644 (file)
index 0000000..f969366
--- /dev/null
@@ -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 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 (file)
index 0000000..6195a1f
--- /dev/null
@@ -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/components/layout/PageMainInner.js b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
new file mode 100644 (file)
index 0000000..41beed6
--- /dev/null
@@ -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 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 (file)
index 0000000..0488fbf
--- /dev/null
@@ -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;
index d47b6e82ba40d6848df42495d823bd8ea91d6319..b7b35b539c54e9bb54885bde54e890c421425304 100644 (file)
@@ -20,6 +20,7 @@
 import $ from 'jquery';
 import { throttle } from 'lodash';
 import Marionette from 'backbone.marionette';
+import key from 'keymaster';
 
 const BOTTOM_OFFSET = 60;
 
index 807abd3d11e8f23062ebd1c18a47d0d7c73cd546..ac723440c9ccc003eb84b381a8a673fbc022f76e 100644 (file)
@@ -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/components/shared/QualifierIcon.js b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js
new file mode 100644 (file)
index 0000000..82ed9f7
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 classNames from 'classnames';
+
+type Props = {
+  className?: string,
+  qualifier: ?string
+};
+
+export default class QualifierIcon extends React.PureComponent {
+  props: Props;
+
+  render() {
+    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/components/shared/__tests__/QualifierIcon-test.js b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js
new file mode 100644 (file)
index 0000000..857ccba
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 QualifierIcon from '../QualifierIcon';
+
+it('should render icon', () => {
+  expect(shallow(<QualifierIcon qualifier="TRK" />)).toMatchSnapshot();
+  expect(shallow(<QualifierIcon qualifier="trk" />)).toMatchSnapshot();
+});
+
+it('should not render icon', () => {
+  expect(shallow(<QualifierIcon qualifier={null} />)).toMatchSnapshot();
+});
+
+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 (file)
index 0000000..58ac761
--- /dev/null
@@ -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/components/shared/qualifier-icon.js b/server/sonar-web/src/main/js/components/shared/qualifier-icon.js
deleted file mode 100644 (file)
index d3e8aea..0000000
+++ /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 React from 'react';
-
-export default React.createClass({
-  render() {
-    if (!this.props.qualifier) {
-      return null;
-    }
-    const className = 'icon-qualifier-' + this.props.qualifier.toLowerCase();
-    return <i className={className} />;
-  }
-});
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 (file)
index 0000000..0465447
--- /dev/null
@@ -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
+      }
+    ]
+  });
+});
index c6e54c559d5a1febea15244a7b7f6caaf3b333e9..c2047a572a488cce6e14c60e99fcbb25331d34f6 100644 (file)
@@ -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/handlebars/componentIssuesPermalink.js b/server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js
deleted file mode 100644 (file)
index cd9aaf5..0000000
+++ /dev/null
@@ -1,22 +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.
- */
-module.exports = function(componentKey) {
-  return window.baseUrl + '/component_issues/index?id=' + encodeURIComponent(componentKey);
-};
diff --git a/server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js b/server/sonar-web/src/main/js/helpers/handlebars/issueFilterHomeLink.js
deleted file mode 100644 (file)
index c65b1c7..0000000
+++ /dev/null
@@ -1,22 +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.
- */
-module.exports = function(id) {
-  return window.baseUrl + '/issues/search#id=' + id;
-};
index e8ce3a697ec11827158ba58fe11f54a0355cfedd..013831fc1e6bfc690d4850bc28e05588a5f08c3d 100644 (file)
@@ -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'),
index 63153bf6dc8695fa5f4858c8aade5ab8934a587b..b65c049f3e47956189d53012fd4723f0f069709e 100644 (file)
@@ -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 '';
+  }
+}
index 602eb3c5786aaee48502fc838fbe2c3d9f656911..96b718cc64ba1fc1640b43ff06b811d7a46e3d1a 100644 (file)
@@ -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);
+};
index 34b3b06d89df974815b49747a88f2bbcbdf27699..f8e127b89a0b76293508c40338964d99bab85687 100644 (file)
@@ -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 (file)
index 8ba7aad..0000000
+++ /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 (file)
index b559d81..0000000
+++ /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];
index 95db709a54531d9c6c174d0663aba15687eefe06..5404ef78f6a6bf55ab346264ab4e8f8887a512b7 100644 (file)
@@ -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);
 
index 658462e620c80c8cd41340a2403e98a755aacf79..ba09fadce81a60d70a05730b25178ab83ada31d5 100644 (file)
@@ -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);
index 9e24c26252f6a9473ae1588a5813b12f11ed1877..cd284d7a54650c0c9b375d23eb0e7b927d6105ef 100644 (file)
 }
 
 .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 {
 
   .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; }
+      }
     }
   }
 }
index 0f496e61f585f194d2e1c815a57273927d475245..a29d4113e52a8231c295af0960f1c70f5f1e7fa6 100644 (file)
   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;
   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;
 }
 
   background-color: #fcfcfc;
 }
 
+.Select-aria-only {
+  display: none;
+}
+
 @keyframes Select-animation-spin {
   to {
     transform: rotate(1turn);
index 08d9a90c69a8dfb607666c0df841ac81b974a973..8b0203bfd2a88c58818e312cbde01135e1b017f6 100644 (file)
   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;
   }
 .search-navigator-date-facet-selection {
   .clearfix;
   position: relative;
+  padding: 0 10px;
   font-size: @smallFontSize;
 }
 
index 3c25afc7d905ee92304a40658f3c7aeb3555a564..c6b9336153e95245ebe132e65c330b259c90741b 100644 (file)
     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 {
   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 {
index e102c368f61419f71c7df361fedd6584d1b97495..382177d6933665fa5e39b2acf9fb95cca74169b2 100644 (file)
@@ -818,14 +818,14 @@ babel-register@^6.22.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.2"
 
-babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.23.0, babel-runtime@^6.9.0:
+babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.20.0, babel-runtime@^6.23.0, babel-runtime@^6.9.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
   dependencies:
     core-js "^2.4.0"
     regenerator-runtime "^0.10.0"
 
-babel-runtime@^6.11.6, babel-runtime@^6.22.0:
+babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
   dependencies:
@@ -2365,7 +2365,7 @@ fbjs@0.1.0-alpha.10:
     promise "^7.0.3"
     whatwg-fetch "^0.9.0"
 
-fbjs@^0.8.1, fbjs@^0.8.4:
+fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8:
   version "0.8.8"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6"
   dependencies:
@@ -2619,6 +2619,14 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+glamor@2.20.24:
+  version "2.20.24"
+  resolved "https://repox.sonarsource.com/api/npm/npm/glamor/-/glamor-2.20.24.tgz#a299af2eec687322634ba38e4a0854d8743d2041"
+  dependencies:
+    babel-runtime "^6.18.0"
+    fbjs "^0.8.8"
+    object-assign "^4.1.0"
+
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3629,6 +3637,10 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4:
     acorn-jsx "^3.0.1"
     object-assign "^4.1.0"
 
+keymaster@1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/keymaster/-/keymaster-1.6.2.tgz#e1ae54d0ea9488f9f60b66b668f02e9a1946c6eb"
+
 kind-of@^3.0.2:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
index 9804977b60724535be7710914db00d78ee8cfc26..a4456f9f574f93c7f348c54ff1019241f13d6908 100644 (file)
@@ -74,6 +74,7 @@ files=Files
 filters=Filters
 filter_verb=Filter
 follow=Follow
+from=From
 global=Global
 help=Help
 hide=Hide
@@ -225,6 +226,7 @@ bulk_change=Bulk Change
 bulleted_point=Bulleted point
 check_project=Check project
 coding_rules=Rules
+clear_all_filters=Clear All Filters
 click_to_add_to_favorites=Click to add to favorites
 click_to_remove_from_favorites=Click to remove from favorites
 contact_admin=Please contact your administrator.
@@ -255,7 +257,8 @@ new_window=New window
 no_data=No data
 no_lines_match_your_filter_criteria=No lines match your filter criteria.
 no_results=No results
-no_results_search=No results. Try to modify the search query to get some results.
+no_results_search=We couldn't find any results matching selected criteria.
+no_results_search.2=Try to change filters to get some results.
 not_authorized=You are not authorized to access this page.
 not_authorized_to_access_project=You are not authorized to access to this '{0}' project
 over_x_days=over {0} days
@@ -687,6 +690,10 @@ issues.toggle_selection_tooltip=(De-)Select all currently visible issues
 issues.ordered=ordered
 issues.by_creation_date=by creation date
 issues.issues=issues
+issues.to_select_issues=to select issues
+issues.to_navigate=to navigate
+issues.leak_period=Leak Period
+issues.my_issues=My Issues
 
 
 #------------------------------------------------------------------------------
@@ -774,11 +781,11 @@ issues.found=Found
 #------------------------------------------------------------------------------
 issues.facet.types=Type
 issues.facet.severities=Severity
-issues.facet.projectUuids=Project
+issues.facet.projects=Project
 issues.facet.statuses=Status
 issues.facet.assignees=Assignee
-issues.facet.fileUuids=File
-issues.facet.moduleUuids=Module
+issues.facet.files=File
+issues.facet.modules=Module
 issues.facet.directories=Directory
 issues.facet.tags=Tag
 issues.facet.rules=Rule
@@ -793,7 +800,7 @@ issues.facet.createdAt.last_year=Last year
 issues.facet.authors=Author
 issues.facet.issues=Issue Key
 issues.facet.mode=Display Mode
-issues.facet.mode.issues=Issues
+issues.facet.mode.count=Issues
 issues.facet.mode.effort=Effort
 
 
@@ -824,10 +831,7 @@ all-projects.results_not_display_due_to_security=Due to security settings, some
 projects.page=Projects
 projects.page.description=Explore projects by coverage, duplications or size.
 projects._projects=projects
-projects.no_projects.1=We couldn't find any projects matching selected criteria.
-projects.no_projects.2=Try to change filters to get some results.
 projects.no_projects.empty_instance=Once you analyze some projects, they will show up here.
-projects.clear_all_filters=Clear All Filters
 projects.no_favorite_projects=You don't have any favorite projects yet.
 projects.no_favorite_projects.engagement=Discover and mark as favorites projects you are interested in to have a quick access to them.
 projects.explore_projects=Explore Projects
index 357a292db1b5bd78e3ba829fe0001a464dd4f523..658e2c57ab7588210d9fc2131510e54e19526b02 100644 (file)
@@ -68,7 +68,7 @@ public class WebTest extends PerfTestCase {
 
   @Test
   public void issues_search() throws Exception {
-    PageStats counters = request("/issues/search");
+    PageStats counters = request("/issues");
     assertDurationLessThan(counters.durationMs, 300);
   }
 
@@ -104,7 +104,7 @@ public class WebTest extends PerfTestCase {
 
   @Test
   public void struts_issues() throws Exception {
-    PageStats counters = request("/issues/search?componentRoots=org.apache.struts:struts-parent");
+    PageStats counters = request("/issues?componentKeys=org.apache.struts:struts-parent");
     assertDurationLessThan(counters.durationMs, 300);
   }
 
index 96c0d296960b7a80360e56232ce201dd6f17c54d..4f08a98eeff251722866040608f2509725abf68e 100644 (file)
@@ -157,7 +157,7 @@ public class UpgradeTest {
     checkUrlIsRedirectedToMaintenancePage("/");
     checkUrlIsRedirectedToMaintenancePage("/issues/index");
     checkUrlIsRedirectedToMaintenancePage("/dashboard/index/org.apache.struts:struts-parent");
-    checkUrlIsRedirectedToMaintenancePage("/issues/search");
+    checkUrlIsRedirectedToMaintenancePage("/issues");
     checkUrlIsRedirectedToMaintenancePage(
       "/component/index?id=org.apache.struts%3Astruts-core%3Asrc%2Fmain%2Fjava%2Forg%2Fapache%2Fstruts%2Fchain%2Fcommands%2Fgeneric%2FWrappingLookupCommand.java");
     checkUrlIsRedirectedToMaintenancePage("/profiles");
@@ -183,7 +183,7 @@ public class UpgradeTest {
     testUrl("/api/qualityprofiles/search");
     testUrl("/issues/index");
     testUrl("/dashboard/index/org.apache.struts:struts-parent");
-    testUrl("/issues/search");
+    testUrl("/issues");
     testUrl("/component/index?id=org.apache.struts%3Astruts-core%3Asrc%2Fmain%2Fjava%2Forg%2Fapache%2Fstruts%2Fchain%2Fcommands%2Fgeneric%2FWrappingLookupCommand.java");
     testUrl("/profiles");
   }