From 139261bbc13192621ef795d6d45298e1d8e1b7f3 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 3 Apr 2017 17:56:23 +0200 Subject: [PATCH] SONAR-9064 Rework facets sidebar on the issues page --- .../java/it/issue/IssueNotificationsTest.java | 6 +- .../test/java/it/issue/IssueSearchTest.java | 18 - it/it-tests/src/test/java/it/ui/UiTest.java | 6 +- .../java/pageobjects/issues/IssuesPage.java | 17 +- .../issue/IssueSearchTest/bulk_change.html | 88 --- ...irect_to_search_url_after_wrong_login.html | 83 --- .../AbstractNewIssuesEmailTemplate.java | 2 +- .../IssueChangesEmailTemplate.java | 2 +- .../MyNewIssuesEmailTemplate.java | 2 +- ...lay_component_key_if_no_component_name.txt | 2 +- ...email_should_display_resolution_change.txt | 2 +- .../email_with_action_plan_change.txt | 2 +- .../email_with_assignee_change.txt | 2 +- .../email_with_multiple_changes.txt | 2 +- .../email_with_all_details.txt | 2 +- ...email_with_no_assignee_tags_components.txt | 2 +- .../email_with_all_details.txt | 2 +- .../email_with_partial_details.txt | 2 +- server/sonar-web/.eslintrc | 1 - .../config/webpack/webpack.config.base.js | 3 +- server/sonar-web/package.json | 2 + .../sonar-web/src/main/js/api/components.js | 3 + server/sonar-web/src/main/js/api/issues.js | 15 +- .../nav/component/ComponentNavBreadcrumbs.js | 2 +- .../nav/component/ComponentNavMenu.js | 9 +- .../ComponentNavBreadcrumbs-test.js.snap | 4 +- .../ComponentNavMenu-test.js.snap | 3 +- .../components/nav/global/GlobalNavMenu.js | 13 +- .../components/nav/global/GlobalNavSearch.js | 1 + .../app/components/nav/global/SearchView.js | 2 +- .../global/__tests__/GlobalNavMenu-test.js | 4 +- .../__snapshots__/GlobalNavMenu-test.js.snap | 10 +- .../src/main/js/app/utils/startReactApp.js | 9 +- .../apps/about/components/EntryIssueTypes.js | 6 +- .../EntryIssueTypesForSonarQubeDotCom.js | 6 +- .../components/TaskComponent.js | 2 +- .../js/apps/code/components/ComponentName.js | 2 +- .../main/js/apps/coding-rules/controller.js | 1 + .../src/main/js/apps/coding-rules/init.js | 3 +- .../js/apps/coding-rules/rule-details-view.js | 1 + .../apps/coding-rules/workspace-list-view.js | 1 + .../src/main/js/apps/component-issues/init.js | 130 ---- .../details/drilldown/Breadcrumb.js | 2 +- .../details/drilldown/ComponentCell.js | 2 +- .../src/main/js/apps/issues/BulkChangeForm.js | 303 -------- .../src/main/js/apps/issues/HeaderView.js | 60 -- .../js/apps/issues/component-viewer/main.js | 132 ---- .../src/main/js/apps/issues/components/App.js | 649 ++++++++++++++++++ .../components/AppContainer.js} | 52 +- .../apps/issues/components/BulkChangeModal.js | 522 ++++++++++++++ .../issues/components/ComponentBreadcrumbs.js | 72 ++ ...IssuesAppContainer.js => FiltersHeader.js} | 50 +- .../js/apps/issues/components/HeaderPanel.js | 106 +++ .../js/apps/issues/components/IssuesList.js | 62 ++ .../issues/components/IssuesSourceViewer.js | 68 ++ .../js/apps/issues/components/ListItem.js | 106 +++ .../apps/issues/components/MyIssuesFilter.js | 60 ++ .../js/apps/issues/components/PageActions.js | 77 +++ .../js/apps/issues/components/SearchSelect.js | 122 ++++ .../components/__tests__/SearchSelect-test.js | 49 ++ .../__snapshots__/SearchSelect-test.js.snap | 48 ++ .../src/main/js/apps/issues/controller.js | 217 ------ .../src/main/js/apps/issues/facets-view.js | 63 -- .../js/apps/issues/facets/assignee-facet.js | 135 ---- .../js/apps/issues/facets/author-facet.js | 60 -- .../main/js/apps/issues/facets/base-facet.js | 41 -- .../js/apps/issues/facets/context-facet.js | 32 - .../apps/issues/facets/creation-date-facet.js | 176 ----- .../apps/issues/facets/custom-values-facet.js | 85 --- .../main/js/apps/issues/facets/file-facet.js | 61 -- .../js/apps/issues/facets/issue-key-facet.js | 40 -- .../js/apps/issues/facets/language-facet.js | 84 --- .../js/apps/issues/facets/module-facet.js | 46 -- .../js/apps/issues/facets/project-facet.js | 112 --- .../js/apps/issues/facets/reporter-facet.js | 61 -- .../js/apps/issues/facets/resolution-facet.js | 65 -- .../main/js/apps/issues/facets/rule-facet.js | 95 --- .../js/apps/issues/facets/severity-facet.js | 31 - .../js/apps/issues/facets/status-facet.js | 31 - .../main/js/apps/issues/facets/tag-facet.js | 72 -- .../main/js/apps/issues/facets/type-facet.js | 31 - .../sonar-web/src/main/js/apps/issues/init.js | 87 --- .../main/js/apps/issues/issue-filter-view.js | 40 -- .../src/main/js/apps/issues/layout.js | 62 -- .../src/main/js/apps/issues/models/issue.js | 30 - .../src/main/js/apps/issues/models/issues.js | 108 --- .../src/main/js/apps/issues/models/state.js | 81 --- .../src/main/js/apps/issues/redirects.js | 55 ++ .../src/main/js/apps/issues/router.js | 35 - .../src/main/js/apps/issues/routes.js | 13 +- .../js/apps/issues/sidebar/AssigneeFacet.js | 168 +++++ .../js/apps/issues/sidebar/AuthorFacet.js | 99 +++ .../apps/issues/sidebar/CreationDateFacet.js | 276 ++++++++ .../js/apps/issues/sidebar/DirectoryFacet.js | 120 ++++ .../main/js/apps/issues/sidebar/FacetMode.js | 67 ++ .../main/js/apps/issues/sidebar/FileFacet.js | 116 ++++ .../js/apps/issues/sidebar/LanguageFacet.js | 114 +++ .../issues/sidebar/LanguageFacetFooter.js | 68 ++ .../js/apps/issues/sidebar/ModuleFacet.js | 113 +++ .../js/apps/issues/sidebar/ProjectFacet.js | 160 +++++ .../js/apps/issues/sidebar/ResolutionFacet.js | 119 ++++ .../main/js/apps/issues/sidebar/RuleFacet.js | 126 ++++ .../sidebar/SeverityFacet.js | 43 +- .../main/js/apps/issues/sidebar/Sidebar.js | 212 ++++++ .../js/apps/issues/sidebar/StatusFacet.js | 112 +++ .../main/js/apps/issues/sidebar/TagFacet.js | 123 ++++ .../main/js/apps/issues/sidebar/TypeFacet.js | 102 +++ .../sidebar/__tests__/AssigneeFacet-test.js | 95 +++ .../issues/sidebar/__tests__/Sidebar-test.js | 53 ++ .../__snapshots__/AssigneeFacet-test.js.snap | 167 +++++ .../__snapshots__/Sidebar-test.js.snap | 112 +++ .../issues/sidebar/components/FacetBox.js | 34 + .../issues/sidebar/components/FacetFooter.js} | 33 +- .../issues/sidebar/components/FacetHeader.js | 83 +++ .../issues/sidebar/components/FacetItem.js | 71 ++ .../sidebar/components/FacetItemsList.js} | 24 +- .../components/__tests__/FacetBox-test.js} | 15 +- .../components/__tests__/FacetFooter-test.js} | 13 +- .../components/__tests__/FacetHeader-test.js | 61 ++ .../components/__tests__/FacetItem-test.js | 68 ++ .../__tests__/FacetItemsList-test.js} | 11 +- .../__snapshots__/FacetBox-test.js.snap | 7 + .../__snapshots__/FacetFooter-test.js.snap | 10 + .../__snapshots__/FacetHeader-test.js.snap | 132 ++++ .../__snapshots__/FacetItem-test.js.snap | 90 +++ .../__snapshots__/FacetItemsList-test.js.snap | 6 + .../src/main/js/apps/issues/styles.css | 23 - .../apps/issues/templates/BulkChangeForm.hbs | 145 ---- .../templates/facets/_issues-facet-header.hbs | 4 - .../facets/issues-assignee-facet.hbs | 26 - .../templates/facets/issues-base-facet.hbs | 11 - .../templates/facets/issues-context-facet.hbs | 3 - .../facets/issues-creation-date-facet.hbs | 42 -- .../facets/issues-custom-values-facet.hbs | 16 - .../templates/facets/issues-file-facet.hbs | 12 - .../facets/issues-issue-key-facet.hbs | 7 - .../templates/facets/issues-mode-facet.hbs | 15 - .../facets/issues-my-issues-facet.hbs | 12 - .../facets/issues-projects-facet.hbs | 16 - .../facets/issues-resolution-facet.hbs | 24 - .../facets/issues-severity-facet.hbs | 13 - .../templates/facets/issues-status-facet.hbs | 13 - .../templates/facets/issues-type-facet.hbs | 13 - .../templates/issues-issue-filter-form.hbs | 89 --- .../apps/issues/templates/issues-layout.hbs | 12 - .../templates/issues-workspace-header.hbs | 80 --- .../issues-workspace-list-component.hbs | 26 - .../templates/issues-workspace-list.hbs | 5 - .../src/main/js/apps/issues/utils.js | 229 ++++++ .../js/apps/issues/workspace-header-view.js | 151 ---- .../apps/issues/workspace-list-item-view.js | 162 ----- .../js/apps/issues/workspace-list-view.js | 125 ---- .../OrganizationFavoriteProjects.js | 2 +- .../components/OrganizationProjects.js | 2 +- .../QualityGateCondition-test.js.snap | 73 +- .../components/ActionsCell.js | 2 +- .../project-admin/key/FineGrainedUpdate.js | 2 +- .../main/js/apps/projects-admin/projects.js | 2 +- .../apps/projects/components/AllProjects.js | 62 +- .../main/js/apps/projects/components/App.js | 2 +- .../apps/projects/components/PageSidebar.js | 4 +- .../apps/projects/components/ProjectsList.js | 4 +- .../__snapshots__/PageSidebar-test.js.snap | 2 +- .../src/main/js/apps/projects/styles.css | 8 - .../details/ProfileProjects.js | 2 +- .../components/SourceViewer/SourceViewer.js | 8 +- .../SourceViewer/SourceViewerBase.js | 19 +- .../SourceViewer/SourceViewerCode.js | 20 +- .../SourceViewer/SourceViewerHeader.js | 10 +- .../SourceViewer/components/Line.js | 19 +- .../SourceViewer/components/LineCode.js | 17 +- .../components/LineIssuesIndicator.js | 3 +- .../SourceViewer/components/LineIssuesList.js | 19 +- .../components/__tests__/LineCode-test.js | 4 +- .../__tests__/LineIssuesList-test.js | 9 +- .../__snapshots__/LineCode-test.js.snap | 10 +- .../__snapshots__/LineIssuesList-test.js.snap | 16 +- .../SourceViewer/helpers/indexing.js | 2 +- .../js/components/__tests__/issue-test.js | 197 ------ .../main/js/components/charts/bar-chart.js | 20 +- .../components/charts/treemap-breadcrumbs.js | 2 +- .../main/js/components/common/EmptySearch.js | 39 ++ .../main/js/components/common/MarkdownTips.js | 2 +- .../main/js/components/common/SelectList.js | 77 ++- .../common/__tests__/SelectList-test.js | 6 +- .../__snapshots__/SelectList-test.js.snap | 8 +- .../components/common/action-options-view.js | 1 + .../src/main/js/components/common/modals.js | 1 + .../src/main/js/components/common/popup.js | 1 + .../main/js/components/controls/Checkbox.js | 6 +- .../main/js/components/controls/DateInput.js | 6 +- .../src/main/js/components/issue/BaseIssue.js | 153 ----- .../src/main/js/components/issue/Issue.js | 145 +++- .../src/main/js/components/issue/IssueView.js | 48 +- .../src/main/js/components/issue/actions.js | 60 +- .../js/components/issue/collections/issues.js | 101 --- .../issue/components/IssueActionsBar.js | 22 +- .../issue/components/IssueCommentAction.js | 11 +- .../components/issue/components/IssueTags.js | 10 +- .../issue/components/IssueTitleBar.js | 32 +- .../issue/components/IssueTransition.js | 12 +- .../issue/components/SimilarIssuesFilter.js | 69 ++ .../__tests__/IssueChangelog-test.js | 7 +- .../__tests__/IssueCommentLine-test.js | 3 +- .../__tests__/IssueTitleBar-test.js | 2 +- .../__snapshots__/IssueTitleBar-test.js.snap | 55 +- .../main/js/components/issue/issue-view.js | 319 --------- .../main/js/components/issue/models/issue.js | 281 -------- .../issue/popups/SimilarIssuesPopup.js | 137 ++++ .../popups/__tests__/ChangelogPopup-test.js | 2 + .../issue/templates/DeleteComment.hbs | 6 - .../issue/templates/comment-form.hbs | 18 - .../templates/issue-assign-form-option.hbs | 5 - .../issue/templates/issue-assign-form.hbs | 10 - .../issue/templates/issue-changelog.hbs | 37 - .../issue/templates/issue-plan-form.hbs | 13 - .../templates/issue-set-severity-form.hbs | 11 - .../issue/templates/issue-set-type-form.hbs | 11 - .../templates/issue-tags-form-option.hbs | 17 - .../issue/templates/issue-tags-form.hbs | 10 - .../templates/issue-transitions-form.hbs | 12 - .../js/components/issue/templates/issue.hbs | 182 ----- .../src/main/js/components/issue/types.js | 11 + .../issue/views/DeleteCommentView.js | 34 - .../issue/views/assign-form-view.js | 172 ----- .../components/issue/views/changelog-view.js | 36 - .../issue/views/comment-form-view.js | 113 --- .../js/components/issue/views/issue-popup.js | 46 -- .../issue/views/set-severity-form-view.js | 51 -- .../issue/views/set-type-form-view.js | 51 -- .../components/issue/views/tags-form-view.js | 196 ------ .../issue/views/transitions-form-view.js | 40 -- .../Page.js} | 25 +- .../layout/PageFilters.js} | 16 +- .../src/main/js/components/layout/PageMain.js | 34 + .../layout/PageMainInner.js} | 21 +- .../src/main/js/components/layout/PageSide.js | 73 ++ .../navigator/workspace-list-view.js | 1 + .../main/js/components/shared/Organization.js | 5 +- .../shared/QualifierIcon.js} | 25 +- .../shared/__tests__/QualifierIcon-test.js} | 29 +- .../__snapshots__/QualifierIcon-test.js.snap | 16 + .../main/js/helpers/__tests__/issues-test.js | 61 ++ .../main/js/helpers/__tests__/urls-test.js | 31 +- .../sonar-web/src/main/js/helpers/issues.js | 18 +- server/sonar-web/src/main/js/helpers/path.js | 9 + .../src/main/js/helpers/testUtils.js | 7 +- server/sonar-web/src/main/js/helpers/urls.js | 25 +- .../src/main/js/libs/third-party/keymaster.js | 314 --------- .../src/main/js/store/issues/duck.js | 50 -- .../src/main/js/store/rootReducer.js | 4 - .../src/main/less/components/issues.less | 6 +- .../src/main/less/components/page.less | 16 +- .../main/less/components/react-select.less | 38 +- .../less/components/search-navigator.less | 24 +- .../sonar-web/src/main/less/pages/issues.less | 29 +- server/sonar-web/yarn.lock | 18 +- .../resources/org/sonar/l10n/core.properties | 20 +- .../sonarqube/perf/server/WebTest.java | 4 +- .../sonarqube/upgrade/UpgradeTest.java | 4 +- 260 files changed, 6779 insertions(+), 6947 deletions(-) delete mode 100644 it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html delete mode 100644 it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html delete mode 100644 server/sonar-web/src/main/js/apps/component-issues/init.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/HeaderView.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/component-viewer/main.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/App.js rename server/sonar-web/src/main/js/apps/{component-issues/components/ComponentIssuesAppContainer.js => issues/components/AppContainer.js} (50%) create mode 100644 server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js rename server/sonar-web/src/main/js/apps/issues/components/{IssuesAppContainer.js => FiltersHeader.js} (53%) create mode 100644 server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/IssuesList.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/ListItem.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/PageActions.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js create mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/controller.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets-view.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/author-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/base-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/context-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/file-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/language-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/module-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/project-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/status-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/facets/type-facet.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/init.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/issue-filter-view.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/layout.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/models/issue.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/models/issues.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/models/state.js create mode 100644 server/sonar-web/src/main/js/apps/issues/redirects.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/router.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js rename server/sonar-web/src/main/js/apps/{issues2 => issues}/sidebar/SeverityFacet.js (75%) create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js rename server/sonar-web/src/main/js/{components/issue/ConnectedIssue.js => apps/issues/sidebar/components/FacetFooter.js} (60%) create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js rename server/sonar-web/src/main/js/apps/{component-issues/routes.js => issues/sidebar/components/FacetItemsList.js} (77%) rename server/sonar-web/src/main/js/{components/issue/models/changelog.js => apps/issues/sidebar/components/__tests__/FacetBox-test.js} (79%) rename server/sonar-web/src/main/js/{components/shared/qualifier-icon.js => apps/issues/sidebar/components/__tests__/FacetFooter-test.js} (79%) create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js rename server/sonar-web/src/main/js/{helpers/handlebars/issueFilterHomeLink.js => apps/issues/sidebar/components/__tests__/FacetItemsList-test.js} (78%) create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/styles.css delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs delete mode 100644 server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs create mode 100644 server/sonar-web/src/main/js/apps/issues/utils.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/workspace-header-view.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js delete mode 100644 server/sonar-web/src/main/js/apps/issues/workspace-list-view.js delete mode 100644 server/sonar-web/src/main/js/components/__tests__/issue-test.js create mode 100644 server/sonar-web/src/main/js/components/common/EmptySearch.js delete mode 100644 server/sonar-web/src/main/js/components/issue/BaseIssue.js delete mode 100644 server/sonar-web/src/main/js/components/issue/collections/issues.js create mode 100644 server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js delete mode 100644 server/sonar-web/src/main/js/components/issue/issue-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/models/issue.js create mode 100644 server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/templates/issue.hbs delete mode 100644 server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/assign-form-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/changelog-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/comment-form-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/issue-popup.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/tags-form-view.js delete mode 100644 server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js rename server/sonar-web/src/main/js/components/{SourceViewer/components/LineIssuesIndicatorContainer.js => layout/Page.js} (67%) rename server/sonar-web/src/main/js/{helpers/handlebars/componentIssuesPermalink.js => components/layout/PageFilters.js} (76%) create mode 100644 server/sonar-web/src/main/js/components/layout/PageMain.js rename server/sonar-web/src/main/js/{apps/issues/workspace-list-empty-view.js => components/layout/PageMainInner.js} (75%) create mode 100644 server/sonar-web/src/main/js/components/layout/PageSide.js rename server/sonar-web/src/main/js/{apps/projects/components/NoProjects.js => components/shared/QualifierIcon.js} (68%) rename server/sonar-web/src/main/js/{apps/issues/facets/mode-facet.js => components/shared/__tests__/QualifierIcon-test.js} (60%) create mode 100644 server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/issues-test.js delete mode 100644 server/sonar-web/src/main/js/libs/third-party/keymaster.js delete mode 100644 server/sonar-web/src/main/js/store/issues/duck.js diff --git a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java index 94059da5a26..ee865958c23 100644 --- a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java @@ -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(""); 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(); } diff --git a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java index de638136033..5c4007cf222 100644 --- a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java @@ -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 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 searchByRuleKey(String... ruleKey) throws IOException { return searchIssues(new SearchWsRequest().setRules(asList(ruleKey))).getIssuesList(); } diff --git a/it/it-tests/src/test/java/it/ui/UiTest.java b/it/it-tests/src/test/java/it/ui/UiTest.java index 4c789853776..b75534fd12f 100644 --- a/it/it-tests/src/test/java/it/ui/UiTest.java +++ b/it/it-tests/src/test/java/it/ui/UiTest.java @@ -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(); diff --git a/it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java b/it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java index 801c24a9b0b..33972cc2823 100644 --- a/it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java +++ b/it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java @@ -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 index c390c02d105..00000000000 --- a/it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
open/sessions/logout
open/sessions/new
waitForTextcontent*Log In to SonarQube*
typeid=loginadmin
typeid=passwordadmin
clickAndWaitcommit
waitForElementPresentcss=.js-user-authenticated
open/issues
waitForElementPresentcss=.search-navigator-workspace-list .issue
waitForElementPresentid=issues-bulk-change
clickid=issues-bulk-change
waitForElementPresentcss=#issues-bulk-change + .dropdown-menu .js-bulk-change
clickcss=#issues-bulk-change + .dropdown-menu .js-bulk-change
waitForElementPresentid=bulk-change-form
waitForElementPresentid=transition-confirm
- - 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 index 791967ed6ea..00000000000 --- a/it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
open/sessions/logout
open/issues#resolved=true|statuses=OPEN
waitForTextcontent*Log In to SonarQube*
waitForElementPresentid=login
typeid=passwordwrongpassword
typeid=loginwronglogin
typeid=passwordwrongpassword
clickAndWaitcommit
waitForTextcss=.alert*Authentication failed*
waitForElementPresentid=login
typeid=loginadmin
typeid=passwordadmin
clickAndWaitcommit
assertLocation*#resolved=true|statuses=OPEN*
- - diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java index 490a57d7514..09c6611dcfe 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java @@ -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: ") diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java index bcf9a93317f..348886958b7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java index 756d4b1a67b..e57c8f4beb1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java @@ -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), diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt index 9f90a7d06ab..80431c61fe8 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt @@ -3,4 +3,4 @@ Rule: Avoid Cycles Message: Has 3 cycles -See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE +See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt index 39a302e0f7f..55fac881144 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt @@ -4,4 +4,4 @@ Message: Has 3 cycles Resolution: FIXED (was FALSE-POSITIVE) -See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE +See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt index f9a4356907a..8d41e1cd9f1 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt @@ -4,4 +4,4 @@ Message: Has 3 cycles Action Plan changed to ABC 1.0 -See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE +See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt index fd4c140e55e..482adb0a017 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt @@ -4,4 +4,4 @@ Message: Has 3 cycles Assignee changed to louis -See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE +See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt index 48de7c38585..b4497b7fbe8 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt @@ -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 diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt index c2e53d7a50f..5b020cfc5e8 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt @@ -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 diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt index a502a407567..7d978bdfcaa 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt @@ -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 diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt index 38a8cd6bf68..7c4bb6c0118 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt @@ -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 diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt index 627de81c7b2..6c6e92ee3ce 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt @@ -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 diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index d6e85fb9139..6b892cf526f 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -11,7 +11,6 @@ }, "globals": { - "key": true, "baseUrl": true, "SyntheticInputEvent": true }, diff --git a/server/sonar-web/config/webpack/webpack.config.base.js b/server/sonar-web/config/webpack/webpack.config.base.js index 0de9ca39631..e8b615bccf8 100644 --- a/server/sonar-web/config/webpack/webpack.config.base.js +++ b/server/sonar-web/config/webpack/webpack.config.base.js @@ -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. diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 3defe39abc6..efa2cf9ad24 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -17,9 +17,11 @@ "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", diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index 512f3a1f7ef..c9bea5e039d 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -137,6 +137,9 @@ export function searchProjects(data?: Object) { return getJSON(url, data); } +export const searchComponents = (data?: { q?: string, qualifiers?: string, ps?: number }) => + getJSON('/api/components/search', data); + /** * Change component's key * @param {string} from diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js index 912f8b2f6d3..ae316d8c228 100644 --- a/server/sonar-web/src/main/js/api/issues.js +++ b/server/sonar-web/src/main/js/api/issues.js @@ -41,15 +41,6 @@ type IssuesResponse = { users?: Array<*> }; -export type Transition = - | 'confirm' - | 'unconfirm' - | 'reopen' - | 'resolve' - | 'falsepositive' - | 'wontfix' - | 'close'; - export const searchIssues = (query: {}): Promise => 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> => 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 { const url = '/api/issues/do_transition'; return postJSON(url, data); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js index 2f668b0b8b9..0d4200d6bf5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js @@ -20,7 +20,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; -import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer'; import OrganizationLink from '../../../../components/ui/OrganizationLink'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index 6b5ba3661a8..6c68203c03b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -101,11 +101,14 @@ export default class ComponentNavMenu extends React.Component { ); } - renderComponentIssuesLink() { + renderIssuesLink() { return (
  • {translate('issues.page')} @@ -343,7 +346,7 @@ export default class ComponentNavMenu extends React.Component { return (
      {this.renderDashboardLink()} - {this.renderComponentIssuesLink()} + {this.renderIssuesLink()} {this.renderComponentMeasuresLink()} {this.renderCodeLink()} {this.renderActivityLink()} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap index d9bd895c7b1..810d438e08e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap @@ -4,7 +4,7 @@ exports[`test should not render breadcrumbs with one element 1`] = ` - - diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 1c7f395f741..3a5f67cdada 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js @@ -25,7 +25,10 @@ import { isUserAdmin } from '../../../../helpers/users'; export default class GlobalNavMenu extends React.Component { static propTypes = { appState: React.PropTypes.object.isRequired, - currentUser: React.PropTypes.object.isRequired + currentUser: React.PropTypes.object.isRequired, + location: React.PropTypes.shape({ + pathname: React.PropTypes.string.isRequired + }).isRequired }; static defaultProps = { @@ -59,12 +62,12 @@ export default class GlobalNavMenu extends React.Component { renderIssuesLink() { const query = this.props.currentUser.isLoggedIn - ? '#resolved=false|assigned_to_me=true' - : '#resolved=false'; - const url = '/issues' + query; + ? { myIssues: 'true', resolved: 'false' } + : { resolved: 'false' }; + const active = this.props.location.pathname === 'issues'; return (
    • - + {translate('issues.page')}
    • diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js index cb8349ecca5..d7089b5a536 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js @@ -20,6 +20,7 @@ import Backbone from 'backbone'; import React from 'react'; import { connect } from 'react-redux'; +import key from 'keymaster'; import SearchView from './SearchView'; import { getCurrentUser } from '../../../../store/rootReducer'; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js index 163ec964e26..9277cb5acb5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js @@ -253,7 +253,7 @@ export default Marionette.LayoutView.extend({ getNavigationFindings(q) { const DEFAULT_ITEMS = [ - { name: translate('issues.page'), url: window.baseUrl + '/issues/search' }, + { name: translate('issues.page'), url: window.baseUrl + '/issues' }, { name: translate('layout.measures'), url: window.baseUrl + '/measures/search?qualifiers[]=TRK' diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js index d21f3f609ee..99b97c82c4f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js @@ -30,6 +30,8 @@ it('should work with extensions', () => { isLoggedIn: false, permissions: { global: [] } }; - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap index 2f2dc6dd0f3..30ec923b76a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap @@ -12,10 +12,16 @@ exports[`test should work with extensions 1`] = `
    • + to={ + Object { + "pathname": "/issues", + "query": Object { + "resolved": "false", + }, + } + }> issues.page
    • diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 34f86b21085..932192fabc3 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -44,7 +44,6 @@ import backgroundTasksRoutes from '../../apps/background-tasks/routes'; import codeRoutes from '../../apps/code/routes'; import codingRulesRoutes from '../../apps/coding-rules/routes'; import componentRoutes from '../../apps/component/routes'; -import componentIssuesRoutes from '../../apps/component-issues/routes'; import componentMeasuresRoutes from '../../apps/component-measures/routes'; import customMeasuresRoutes from '../../apps/custom-measures/routes'; import groupsRoutes from '../../apps/groups/routes'; @@ -89,9 +88,8 @@ const startReactApp = () => { { - 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 = () => { /> + @@ -158,7 +157,6 @@ const startReactApp = () => { - @@ -176,6 +174,7 @@ const startReactApp = () => { component={ProjectPageExtension} /> + {projectAdminRoutes} diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js index 3ae7d82992d..fd231aa512b 100644 --- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js +++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js @@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component { {formatMeasure(bugs, 'SHORT_INT')} @@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component { {formatMeasure(vulnerabilities, 'SHORT_INT')} @@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component { {formatMeasure(codeSmells, 'SHORT_INT')} diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js index 3b974ffd770..11db35cac3b 100644 --- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js +++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js @@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { {formatMeasure(bugs, 'SHORT_INT')} @@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { {formatMeasure(vulnerabilities, 'SHORT_INT')} @@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { {formatMeasure(codeSmells, 'SHORT_INT')} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js index 6c7d13c15b7..2a9fb4157ae 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js @@ -21,7 +21,7 @@ import React from 'react'; import { Link } from 'react-router'; import TaskType from './TaskType'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; import Organization from '../../../components/shared/Organization'; import { Task } from '../types'; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js index 9ad27ba80ec..76ea80e652f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js @@ -20,7 +20,7 @@ import React from 'react'; import { Link } from 'react-router'; import Truncated from './Truncated'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; function getTooltip(component) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/controller.js b/server/sonar-web/src/main/js/apps/coding-rules/controller.js index 01943b685bf..dd15fb39b92 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/controller.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/controller.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; +import key from 'keymaster'; import Controller from '../../components/navigator/controller'; import Rule from './models/rule'; import RuleDetailsView from './rule-details-view'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js index 57e689c5178..437505c5eaf 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/init.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/init.js @@ -22,6 +22,7 @@ import $ from 'jquery'; import { sortBy } from 'lodash'; import Backbone from 'backbone'; import Marionette from 'backbone.marionette'; +import key from 'keymaster'; import State from './models/state'; import Layout from './layout'; import Rules from './models/rules'; @@ -105,7 +106,7 @@ App.on('start', function(options: { }); this.layout.filtersRegion.show(this.filtersView); - window.key.setScope('list'); + key.setScope('list'); this.router = new Router({ app: this }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js index 307765e54c6..722e3f22d3e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js @@ -21,6 +21,7 @@ import $ from 'jquery'; import { union } from 'lodash'; import Backbone from 'backbone'; import Marionette from 'backbone.marionette'; +import key from 'keymaster'; import Rules from './models/rules'; import MetaView from './rule/rule-meta-view'; import DescView from './rule/rule-description-view'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js index 1e32a633a9a..b218d6939b9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import key from 'keymaster'; import WorkspaceListView from '../../components/navigator/workspace-list-view'; import WorkspaceListItemView from './workspace-list-item-view'; import WorkspaceListEmptyView from './workspace-list-empty-view'; diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js deleted file mode 100644 index 4b5abd4eb72..00000000000 --- a/server/sonar-web/src/main/js/apps/component-issues/init.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { difference } from 'lodash'; -import Backbone from 'backbone'; -import Marionette from 'backbone.marionette'; -import State from '../issues/models/state'; -import Layout from '../issues/layout'; -import Issues from '../issues/models/issues'; -import Facets from '../../components/navigator/models/facets'; -import Controller from '../issues/controller'; -import Router from '../issues/router'; -import WorkspaceListView from '../issues/workspace-list-view'; -import WorkspaceHeaderView from '../issues/workspace-header-view'; -import FacetsView from './../issues/facets-view'; -import HeaderView from './../issues/HeaderView'; - -const App = new Marionette.Application(); -const init = function({ el, component, currentUser }) { - this.config = { - resource: component.id, - resourceName: component.name, - resourceQualifier: component.qualifier - }; - this.state = new State({ - canBulkChange: currentUser.isLoggedIn, - isContext: true, - contextQuery: { componentUuids: this.config.resource }, - contextComponentUuid: this.config.resource, - contextComponentName: this.config.resourceName, - contextComponentQualifier: this.config.resourceQualifier, - contextOrganization: component.organization - }); - this.updateContextFacets(); - this.list = new Issues(); - this.facets = new Facets(); - - this.layout = new Layout({ app: this, el }); - this.layout.render(); - $('#footer').addClass('search-navigator-footer'); - - this.controller = new Controller({ app: this }); - - this.issuesView = new WorkspaceListView({ - app: this, - collection: this.list - }); - this.layout.workspaceListRegion.show(this.issuesView); - this.issuesView.bindScrollEvents(); - - this.workspaceHeaderView = new WorkspaceHeaderView({ - app: this, - collection: this.list - }); - this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView); - - this.facetsView = new FacetsView({ - app: this, - collection: this.facets - }); - this.layout.facetsRegion.show(this.facetsView); - - this.headerView = new HeaderView({ - app: this - }); - this.layout.filtersRegion.show(this.headerView); - - key.setScope('list'); - App.router = new Router({ app: App }); - Backbone.history.start(); -}; - -App.getContextQuery = function() { - return { componentUuids: this.config.resource }; -}; - -App.getRestrictedFacets = function() { - return { - TRK: ['projectUuids'], - BRC: ['projectUuids'], - DIR: ['projectUuids', 'moduleUuids', 'directories'], - DEV: ['authors'], - DEV_PRJ: ['projectUuids', 'authors'] - }; -}; - -App.updateContextFacets = function() { - const facets = this.state.get('facets'); - const allFacets = this.state.get('allFacets'); - const facetsFromServer = this.state.get('facetsFromServer'); - return this.state.set({ - facets, - allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]), - facetsFromServer: difference( - facetsFromServer, - this.getRestrictedFacets()[this.config.resourceQualifier] - ) - }); -}; - -App.on('start', options => { - init.call(App, options); -}); - -export default function(el, component, currentUser) { - App.start({ el, component, currentUser }); - - return () => { - Backbone.history.stop(); - App.layout.destroy(); - $('#footer').removeClass('search-navigator-footer'); - }; -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js index 03d857462bf..5261badeaf2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { isDiffMetric, formatLeak } from '../../utils'; import { formatMeasure } from '../../../../helpers/measures'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js index 1ccdd7eed1b..43d0d8bd422 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js @@ -19,7 +19,7 @@ */ import React from 'react'; import classNames from 'classnames'; -import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { splitPath } from '../../../../helpers/path'; import { getComponentUrl } from '../../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js deleted file mode 100644 index 1c9b447b231..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import { debounce, sortBy } from 'lodash'; -import ModalForm from '../../components/common/modal-form'; -import Template from './templates/BulkChangeForm.hbs'; -import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; -import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues'; -import { searchUsers } from '../../api/users'; -import { searchMembers } from '../../api/organizations'; -import { translate, translateWithParameters } from '../../helpers/l10n'; - -const LIMIT = 500; -const INPUT_WIDTH = '250px'; -const MINIMUM_QUERY_LENGTH = 2; -const UNASSIGNED = ''; - -type Issue = { - actions?: Array, - assignee: string | null, - transitions?: Array -}; - -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 - ? ` ${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) { - return issues.filter(hasAction('assign')).length; - }, - - canBeAssignedToMe(issues: Array) { - return issues.filter(hasAction('assign_to_me')).length; - }, - - canBeUnassigned(issues: Array) { - return issues.filter(issue => issue.assignee).length; - }, - - canChangeType(issues: Array) { - return issues.filter(hasAction('set_type')).length; - }, - - canChangeSeverity(issues: Array) { - return issues.filter(hasAction('set_severity')).length; - }, - - canChangeTags(issues: Array) { - return issues.filter(hasAction('set_tags')).length; - }, - - canBeCommented(issues: Array) { - return issues.filter(hasAction('comment')).length; - }, - - availableTransitions(issues: Array) { - const transitions = {}; - issues.forEach(issue => { - if (issue.transitions) { - issue.transitions.forEach(t => { - if (transitions[t] != null) { - transitions[t]++; - } else { - transitions[t] = 1; - } - }); - } - }); - return sortBy(Object.keys(transitions)).map(transition => ({ - transition, - count: transitions[transition] - })); - }, - - serializeData() { - return { - ...ModalForm.prototype.serializeData.apply(this, arguments), - isLoaded: this.issues != null && this.tags != null, - issues: this.issues, - limitReached: this.paging && this.paging.total > LIMIT, - canBeAssigned: this.issues && this.canBeAssigned(this.issues), - canChangeType: this.issues && this.canChangeType(this.issues), - canChangeSeverity: this.issues && this.canChangeSeverity(this.issues), - canChangeTags: this.issues && this.canChangeTags(this.issues), - canBeCommented: this.issues && this.canBeCommented(this.issues), - availableTransitions: this.issues && this.availableTransitions(this.issues) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/HeaderView.js b/server/sonar-web/src/main/js/apps/issues/HeaderView.js deleted file mode 100644 index 596996f9d5c..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/HeaderView.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Marionette from 'backbone.marionette'; -import Template from './templates/facets/issues-my-issues-facet.hbs'; - -export default Marionette.ItemView.extend({ - template: Template, - className: 'issues-header-inner', - - events: { - 'change [name="issues-page-my"]': 'onMyIssuesChange' - }, - - initialize() { - this.listenTo(this.options.app.state, 'change:query', this.render); - }, - - onMyIssuesChange() { - const mode = this.$('[name="issues-page-my"]:checked').val(); - if (mode === 'my') { - this.options.app.state.updateFilter({ - assigned_to_me: 'true', - assignees: null, - assigned: null - }); - } else { - this.options.app.state.updateFilter({ - assigned_to_me: null, - assignees: null, - assigned: null - }); - } - }, - serializeData() { - const me = !!this.options.app.state.get('query').assigned_to_me; - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - me, - isContext: this.options.app.state.get('isContext'), - user: this.options.app.state.get('user') - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js deleted file mode 100644 index a1dcb64d7dc..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -import WithStore from '../../../components/shared/WithStore'; - -export default Marionette.ItemView.extend({ - template() { - return '
      '; - }, - - 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( - - - , - this.el - ); - }, - - openFileByIssue(issue) { - this.baseIssue = issue; - this.selectedIssue = issue.get('key'); - this.showViewer(this.scrollToBaseIssue); - this.bindShortcuts(); - }, - - bindShortcuts() { - key('up', 'componentViewer', () => { - this.options.app.controller.selectPrev(); - return false; - }); - key('down', 'componentViewer', () => { - this.options.app.controller.selectNext(); - return false; - }); - key('left,backspace', 'componentViewer', () => { - this.options.app.controller.closeComponentViewer(); - return false; - }); - }, - - unbindShortcuts() { - key.deleteScope('componentViewer'); - }, - - select() { - const selected = this.options.app.state.get('selectedIndex'); - const selectedIssue = this.options.app.list.at(selected); - - if (selectedIssue.get('component') === this.baseIssue.get('component')) { - this.baseIssue = selectedIssue; - this.showViewer(this.scrollToBaseIssue); - this.scrollToBaseIssue(); - } else { - this.options.app.controller.showComponentViewer(selectedIssue); - } - }, - - scrollToLine(line) { - const row = this.$(`[data-line-number=${line}]`); - const topOffset = $(window).height() / 2 - 60; - const goal = row.length > 0 ? row.offset().top - topOffset : 0; - $(window).scrollTop(goal); - }, - - selectIssue(issueKey) { - const issue = this.options.app.list.find(model => model.get('key') === issueKey); - const index = this.options.app.list.indexOf(issue); - this.options.app.state.set({ selectedIndex: index }); - }, - - scrollToBaseIssue() { - this.scrollToLine(this.baseIssue.get('line')); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js new file mode 100644 index 00000000000..73574c611b5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -0,0 +1,649 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Helmet from 'react-helmet'; +import key from 'keymaster'; +import { keyBy, without } from 'lodash'; +import HeaderPanel from './HeaderPanel'; +import PageActions from './PageActions'; +import FiltersHeader from './FiltersHeader'; +import MyIssuesFilter from './MyIssuesFilter'; +import Sidebar from '../sidebar/Sidebar'; +import IssuesList from './IssuesList'; +import ComponentBreadcrumbs from './ComponentBreadcrumbs'; +import IssuesSourceViewer from './IssuesSourceViewer'; +import BulkChangeModal from './BulkChangeModal'; +import { + parseQuery, + areMyIssuesSelected, + areQueriesEqual, + getOpen, + serializeQuery, + parseFacets +} from '../utils'; +import type { + Query, + Paging, + Facet, + ReferencedComponent, + ReferencedUser, + ReferencedLanguage, + Component, + CurrentUser +} from '../utils'; +import ListFooter from '../../../components/controls/ListFooter'; +import EmptySearch from '../../../components/common/EmptySearch'; +import Page from '../../../components/layout/Page'; +import PageMain from '../../../components/layout/PageMain'; +import PageMainInner from '../../../components/layout/PageMainInner'; +import PageSide from '../../../components/layout/PageSide'; +import PageFilters from '../../../components/layout/PageFilters'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { scrollToElement } from '../../../helpers/scrolling'; +import type { Issue } from '../../../components/issue/types'; + +type Props = { + component?: Component, + currentUser: CurrentUser, + fetchIssues: () => Promise<*>, + location: { pathname: string, query: { [string]: string } }, + onRequestFail: (Error) => void, + router: { push: () => void, replace: () => void } +}; + +type State = { + bulkChange: 'all' | 'selected' | null, + checked: Array, + facets: { [string]: Facet }, + issues: Array, + 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, 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> => { + 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, 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 ( +
      + {checked.length > 0 + ? + : } + {bulkChange != null && + } +
      + ); + } + + 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 ( +
      + {paging.total > 0 && + } + + {paging.total > 0 && + } + + {paging.total === 0 && } +
      + ); + } + + 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 ( + + + + + + {currentUser.isLoggedIn && + } + + + + + + + + + {this.renderBulkChange(openIssue)} + {openIssue != null && +
      + +
      } + +
      +
      + + +
      + {openIssue != null && + } + + {this.renderList(openIssue)} +
      +
      +
      +
      + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js similarity index 50% rename from server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js rename to server/sonar-web/src/main/js/apps/issues/components/AppContainer.js index 061b3e8f8c6..c605961dcad 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js @@ -17,36 +17,38 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +// @flow import { connect } from 'react-redux'; -import init from '../init'; +import { withRouter } from 'react-router'; +import type { Dispatch } from 'redux'; +import App from './App'; +import { onFail } from '../../../store/rootActions'; import { getComponent, getCurrentUser } from '../../../store/rootReducer'; +import { searchIssues } from '../../../api/issues'; +import { parseIssueFromResponse } from '../../../helpers/issues'; -class ComponentIssuesAppContainer extends React.Component { - componentDidMount() { - this.stop = init(this.refs.container, this.props.component, this.props.currentUser); - } - - componentWillUnmount() { - this.stop(); - } - - render() { - // placing container inside div is required, - // because when backbone.marionette's layout is destroyed, - // it also destroys the root element, - // but react wants it to be there to unmount it - return ( -
      -
      -
      - ); - } -} +type Query = { [string]: string }; const mapStateToProps = (state, ownProps) => ({ - component: getComponent(state, ownProps.location.query.id), + component: ownProps.location.query.id + ? getComponent(state, ownProps.location.query.id) + : undefined, currentUser: getCurrentUser(state) }); -export default connect(mapStateToProps)(ComponentIssuesAppContainer); +const fetchIssues = (query: Query) => + (dispatch: Dispatch<*>) => + searchIssues({ ...query, additionalFields: '_all' }).then( + response => { + const parsedIssues = response.issues.map(issue => + parseIssueFromResponse(issue, response.components, response.users, response.rules)); + return { ...response, issues: parsedIssues }; + }, + onFail(dispatch) + ); + +const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error); + +const mapDispatchToProps = { fetchIssues, onRequestFail }; + +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App)); diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js new file mode 100644 index 00000000000..3b7d454aa72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js @@ -0,0 +1,522 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import Select from 'react-select'; +import { css } from 'glamor'; +import { pickBy, sortBy } from 'lodash'; +import SearchSelect from './SearchSelect'; +import Checkbox from '../../../components/controls/Checkbox'; +import Tooltip from '../../../components/controls/Tooltip'; +import MarkdownTips from '../../../components/common/MarkdownTips'; +import SeverityHelper from '../../../components/shared/SeverityHelper'; +import Avatar from '../../../components/ui/Avatar'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import { searchIssueTags, bulkChangeIssues } from '../../../api/issues'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { searchAssignees } from '../utils'; +import type { Paging, Component, CurrentUser } from '../utils'; +import type { Issue } from '../../../components/issue/types'; + +type Props = {| + component?: Component, + currentUser: CurrentUser, + fetchIssues: ({}) => Promise<*>, + onClose: () => void, + onDone: () => void, + onRequestFail: (Error) => void +|}; + +type State = {| + issues: Array, + // used for initial loading of issues + loading: boolean, + paging?: Paging, + // used when submitting a form + submitting: boolean, + tags?: Array, + + // form fields + addTags?: Array, + assignee?: string, + comment?: string, + notifications?: boolean, + removeTags?: Array, + 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): 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 = () => ( + + {translate('cancel')} + + ); + + renderLoading = () => ( +
      +
      +

      {translate('bulk_change')}

      +
      +
      +
      + +
      +
      +
      + {this.renderCancelButton()} +
      +
      + ); + + renderCheckbox = (field: string) => ( + + ); + + renderAffected = (affected: number) => ( +
      + ({translateWithParameters('issue_bulk_change.x_issues', affected)}) +
      + ); + + renderField = (field: string, label: string, affected: ?number, input: Object) => ( +
      + + {this.renderCheckbox(field)} + {input} + {affected != null && this.renderAffected(affected)} +
      + ); + + renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => ( + + {(option.avatar != null || option.email != null) && + } + {option.label} + + ); + + renderAssigneeField = () => { + const affected: number = this.state.issues.filter(hasAction('assign')).length; + + if (affected === 0) { + return null; + } + + const input = ( + + ); + + 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 }) => ( + + + {option.label} + + ); + + const input = ( + } + searchable={false} + value={this.state.severity} + valueRenderer={option => } + /> + ); + + 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 = ( + + ); + + return this.renderField('removeTags', 'issue.remove_tags', affected, input); + }; + + renderTransitionsField = () => { + const transitions = this.getAvailableTransitions(this.state.issues); + + if (transitions.length === 0) { + return null; + } + + return ( +
      + + {transitions.map(transition => ( + + + + {this.renderAffected(transition.count)} +
      +
      + ))} +
      + ); + }; + + renderCommentField = () => { + const affected: number = this.state.issues.filter(hasAction('comment')).length; + + if (affected === 0) { + return null; + } + + return ( +
      + +
      + -
      -
      - {{> ../../../components/common/templates/_markdown-tips}} -
      -
      - {{/if}} - - {{! notifications }} - - -
      - - -{{else}} - - - -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs deleted file mode 100644 index 64237bc0e79..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs +++ /dev/null @@ -1,4 +0,0 @@ - - - {{t "issues.facet" property}} - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs deleted file mode 100644 index 9fb23f78ab8..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{> "_issues-facet-header"}} - -
      - {{#each values}} - {{#eq val ""}} - {{! unassigned }} - - {{t "unassigned"}} - - {{formatFacetValue count ../../state.facetMode}} - - - {{else}} - - {{label}} - - {{formatFacetValue count ../../state.facetMode}} - - - {{/eq}} - {{/each}} - -
      - -
      -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs deleted file mode 100644 index 83442f81b37..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{> "_issues-facet-header"}} - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs deleted file mode 100644 index 9f981c07c1a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
      - Issues of    {{qualifierIcon state.contextComponentQualifier}} {{state.contextComponentName}} -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs deleted file mode 100644 index bf5410833c9..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs +++ /dev/null @@ -1,42 +0,0 @@ -{{> "_issues-facet-header"}} - -{{#if createdAt}} - -
      - {{dt createdAt}} ({{fromNow createdAt}}) -
      -{{else}} - -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs deleted file mode 100644 index 0674f5b87bb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs deleted file mode 100644 index 3569040b69c..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs deleted file mode 100644 index 28ae140f75b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{> "_issues-facet-header"}} - -
      -
      - {{issues}} -
      -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs deleted file mode 100644 index af69286d92a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
      - {{t 'issues.facet.mode'}} -
      - - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs deleted file mode 100644 index c11181a70dd..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if user.isLoggedIn}} -
        -
      • - - -
      • -
      • - - -
      • -
      -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs deleted file mode 100644 index 7693af65549..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs deleted file mode 100644 index 19e47071f8b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs deleted file mode 100644 index 88b9bd0585b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs deleted file mode 100644 index cc7f1fcefdb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs deleted file mode 100644 index f97ada41dca..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - - diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs deleted file mode 100644 index f18f6845d3a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs +++ /dev/null @@ -1,89 +0,0 @@ - - - - -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs deleted file mode 100644 index 83add61b366..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
      -
      -
      -
      -
      - -
      -
      -
      -
      -
      -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs deleted file mode 100644 index 6a28b925ad8..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs +++ /dev/null @@ -1,80 +0,0 @@ -
      -
      - {{#if state.component}} - - - -
      - {{#if organization}} - {{organization.name}} - - {{/if}} - {{#with state.component}} - {{#if project}} - {{projectName}} - - {{/if}} - {{#if subProject}} - {{subProjectName}} - - {{/if}} - {{collapsePath name}} - {{/with}} -
      - - {{else}} - {{#if state.canBulkChange}} - - {{else}} -   - {{/if}} - {{/if}} -
      -
      - - -
      - {{#notNull state.total}} - {{#unless state.component}} -
      {{t 'issues.ordered'}} {{t 'issues.by_creation_date'}}
      - {{/unless}} -
      - {{#gt state.total 0}} - - - {{sum state.selectedIndex 1}} - / - {{formatMeasure state.total 'INT'}} - - - {{else}} - 0 / 0 - {{/gt}} - {{t 'issues.issues'}} -
      - {{/notNull}} - - - -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs deleted file mode 100644 index b0e305ecddb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs +++ /dev/null @@ -1,26 +0,0 @@ -
      - {{#if organization}} - - {{organization.name}} - - - {{/if}} - - {{#if project}} - - {{projectLongName}} - - - {{/if}} - - {{#if subProject}} - - {{subProjectLongName}} - - - {{/if}} - - - {{collapsePath componentLongName}} - -
      diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs deleted file mode 100644 index cae9c964a0b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
      - -
      - -
      diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js new file mode 100644 index 00000000000..3c9d8a235ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/utils.js @@ -0,0 +1,229 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import { isNil, omitBy } from 'lodash'; +import { searchMembers } from '../../api/organizations'; +import { searchUsers } from '../../api/users'; + +export type RawQuery = { [string]: string }; + +export type Query = {| + assigned: boolean, + assignees: Array, + authors: Array, + createdAfter: string, + createdAt: string, + createdBefore: string, + createdInLast: string, + directories: Array, + facetMode: string, + files: Array, + issues: Array, + languages: Array, + modules: Array, + projects: Array, + resolved: boolean, + resolutions: Array, + rules: Array, + severities: Array, + sinceLeakPeriod: boolean, + statuses: Array, + tags: Array, + types: Array +|}; + +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 => 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 => 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, b: Array) => { + 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): { [string]: Facet } => { + // for readability purpose + const propertyMapping = { + fileUuids: 'files', + moduleUuids: 'modules', + projectUuids: 'projects' + }; + + const result = {}; + facets.forEach(facet => { + const values = {}; + facet.values.forEach(value => { + values[value.val] = value.count; + }); + const finalProperty = propertyMapping[facet.property] || facet.property; + result[finalProperty] = values; + }); + return result; +}; + +export type ReferencedComponent = { + key: string, + name: string, + organization: string, + path: string +}; + +export type ReferencedUser = { + avatar: string, + name: string +}; + +export type ReferencedLanguage = { + name: string +}; + +export type Component = { + key: string, + organization: string, + qualifier: string +}; + +export type CurrentUser = + | { isLoggedIn: false } + | { isLoggedIn: true, email?: string, login: string, name: string }; + +export const searchAssignees = (query: string, component?: Component) => { + return component + ? searchMembers({ organization: component.organization, ps: 50, q: query }).then(response => + response.users.map(user => ({ + avatar: user.avatar, + label: user.name, + value: user.login + }))) + : searchUsers(query, 50).then(response => + response.users.map(user => ({ + // TODO this WS returns no avatar + avatar: user.avatar, + email: user.email, + label: user.name, + value: user.login + }))); +}; diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js deleted file mode 100644 index 0fccbb3ab65..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import WorkspaceHeaderView from '../../components/navigator/workspace-header-view'; -import BulkChangeForm from './BulkChangeForm'; -import Template from './templates/issues-workspace-header.hbs'; -import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils'; - -export default WorkspaceHeaderView.extend({ - template: Template, - - initialize() { - this.context = { - isContext: this.options.app.state.get('isContext'), - organization: this.options.app.state.get('contextOrganization') - }; - }, - - events() { - return { - ...WorkspaceHeaderView.prototype.events.apply(this, arguments), - 'click .js-selection': 'onSelectionClick', - 'click .js-back': 'returnToList', - 'click .js-new-search': 'newSearch', - 'click .js-bulk-change-selected': 'onBulkChangeSelectedClick' - }; - }, - - onSelectionClick(e) { - e.preventDefault(); - this.toggleSelection(); - }, - - onBulkChangeSelectedClick(e) { - e.preventDefault(); - this.bulkChangeSelected(); - }, - - afterBulkChange() { - const selectedIndex = this.options.app.state.get('selectedIndex'); - const selectedKeys = this.options.app.list.where({ selected: true }).map(item => item.id); - this.options.app.controller.fetchList().done(() => { - this.options.app.state.set({ selectedIndex }); - this.options.app.list.selectByKeys(selectedKeys); - }); - }, - - render() { - if (!this._suppressUpdate) { - WorkspaceHeaderView.prototype.render.apply(this, arguments); - } - }, - - toggleSelection() { - this._suppressUpdate = true; - const selectedCount = this.options.app.list.where({ selected: true }).length; - const someSelected = selectedCount > 0; - return someSelected ? this.selectNone() : this.selectAll(); - }, - - selectNone() { - this.options.app.list.where({ selected: true }).forEach(issue => { - issue.set({ selected: false }); - }); - this._suppressUpdate = false; - this.render(); - }, - - selectAll() { - this.options.app.list.forEach(issue => { - issue.set({ selected: true }); - }); - this._suppressUpdate = false; - this.render(); - }, - - returnToList() { - this.options.app.controller.closeComponentViewer(); - }, - - newSearch() { - this.options.app.controller.newSearch(); - }, - - bulkChange() { - const query = this.options.app.controller.getQueryAsObject(); - new BulkChangeForm({ - query, - context: this.context, - onChange: () => this.afterBulkChange() - }).render(); - }, - - bulkChangeSelected() { - const selected = this.options.app.list.where({ selected: true }); - const selectedKeys = selected.map(item => item.id).slice(0, 500); - const query = { issues: selectedKeys.join() }; - new BulkChangeForm({ - query, - context: this.context, - onChange: () => this.afterBulkChange() - }).render(); - }, - - serializeData() { - const issuesCount = this.options.app.list.length; - const selectedCount = this.options.app.list.where({ selected: true }).length; - const allSelected = issuesCount > 0 && issuesCount === selectedCount; - const someSelected = !allSelected && selectedCount > 0; - const data = { - ...WorkspaceHeaderView.prototype.serializeData.apply(this, arguments), - selectedCount, - allSelected, - someSelected - }; - const component = this.options.app.state.get('component'); - if (component) { - const qualifier = this.options.app.state.get('contextComponentQualifier'); - if (qualifier === 'VW' || qualifier === 'SVW') { - // do nothing - } else if (qualifier === 'TRK') { - data.state.component.project = null; - } else if (qualifier === 'BRC') { - data.state.component.project = null; - data.state.component.subProject = null; - } else { - const organization = areThereCustomOrganizations() - ? getOrganization(component.projectOrganization) - : null; - Object.assign(data, { organization }); - } - } - return data; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js deleted file mode 100644 index ec59af550c0..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import ConnectedIssue from '../../components/issue/ConnectedIssue'; -import IssueFilterView from './issue-filter-view'; -import WithStore from '../../components/shared/WithStore'; -import getStore from '../../app/utils/getStore'; -import { getIssueByKey } from '../../store/rootReducer'; - -const SHOULD_NULL = { - any: ['issues'], - resolutions: ['resolved'], - resolved: ['resolutions'], - assignees: ['assigned'], - assigned: ['assignees'] -}; - -export default Marionette.ItemView.extend({ - className: 'issues-workspace-list-item', - - initialize(options) { - this.openComponentViewer = this.openComponentViewer.bind(this); - this.onIssueFilterClick = this.onIssueFilterClick.bind(this); - this.onIssueCheck = this.onIssueCheck.bind(this); - this.listenTo(options.app.state, 'change:selectedIndex', this.showIssue); - this.listenTo(this.model, 'change:selected', this.showIssue); - this.subscribeToStore(); - }, - - template() { - return '
      '; - }, - - 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( - - - , - this.el - ); - }, - - onIssueFilterClick(e) { - const that = this; - e.preventDefault(); - e.stopPropagation(); - $('body').click(); - this.popup = new IssueFilterView({ - triggerEl: $(e.currentTarget), - bottomRight: true, - model: this.model - }); - this.popup.on('select', (property, value) => { - const obj = {}; - obj[property] = '' + value; - SHOULD_NULL.any.forEach(p => { - obj[p] = null; - }); - if (SHOULD_NULL[property] != null) { - SHOULD_NULL[property].forEach(p => { - obj[p] = null; - }); - } - that.options.app.state.updateFilter(obj); - that.popup.destroy(); - }); - this.popup.render(); - }, - - onIssueCheck(e) { - e.preventDefault(); - e.stopPropagation(); - this.model.set({ selected: !this.model.get('selected') }); - const selected = this.model.collection.where({ selected: true }).length; - this.options.app.state.set({ selected }); - }, - - changeSelection() { - const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - if (selected) { - this.select(); - } else { - this.unselect(); - } - }, - - selectCurrent() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - }, - - resetIssue(options) { - const that = this; - const key = this.model.get('key'); - const componentUuid = this.model.get('componentUuid'); - const index = this.model.get('index'); - const selected = this.model.get('selected'); - this.model.reset( - { - key, - componentUuid, - index, - selected - }, - { silent: true } - ); - return this.model.fetch(options).done(() => that.trigger('reset')); - }, - - openComponentViewer() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - if (this.options.app.state.has('component')) { - return this.options.app.controller.closeComponentViewer(); - } else { - return this.options.app.controller.showComponentViewer(this.model); - } - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js deleted file mode 100644 index f74b7c912b2..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import WorkspaceListView from '../../components/navigator/workspace-list-view'; -import IssueView from './workspace-list-item-view'; -import EmptyView from './workspace-list-empty-view'; -import Template from './templates/issues-workspace-list.hbs'; -import ComponentTemplate from './templates/issues-workspace-list-component.hbs'; -import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils'; - -const COMPONENT_HEIGHT = 29; -const BOTTOM_OFFSET = 60; - -export default WorkspaceListView.extend({ - template: Template, - componentTemplate: ComponentTemplate, - childView: IssueView, - childViewContainer: '.js-list', - emptyView: EmptyView, - - bindShortcuts() { - const that = this; - WorkspaceListView.prototype.bindShortcuts.apply(this, arguments); - key('right', 'list', () => { - const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); - that.options.app.controller.showComponentViewer(selectedIssue); - return false; - }); - key('space', 'list', () => { - const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); - selectedIssue.set({ selected: !selectedIssue.get('selected') }); - return false; - }); - }, - - unbindShortcuts() { - WorkspaceListView.prototype.unbindShortcuts.apply(this, arguments); - key.unbind('right', 'list'); - key.unbind('space', 'list'); - }, - - scrollTo() { - const selectedIssue = this.collection.at(this.options.app.state.get('selectedIndex')); - if (selectedIssue == null) { - return; - } - const selectedIssueView = this.children.findByModel(selectedIssue); - const parentTopOffset = this.$el.offset().top; - let viewTop = selectedIssueView.$el.offset().top - parentTopOffset; - if (selectedIssueView.$el.prev().is('.issues-workspace-list-component')) { - viewTop -= COMPONENT_HEIGHT; - } - const viewBottom = selectedIssueView.$el.offset().top + - selectedIssueView.$el.outerHeight() + - BOTTOM_OFFSET; - const windowTop = $(window).scrollTop(); - const windowBottom = windowTop + $(window).height(); - if (viewTop < windowTop) { - $(window).scrollTop(viewTop); - } - if (viewBottom > windowBottom) { - $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom); - } - }, - - attachHtml(compositeView, childView, index) { - const container = this.getChildViewContainer(compositeView); - const model = this.collection.at(index); - if (model != null) { - const prev = index > 0 && this.collection.at(index - 1); - let putComponent = !prev; - if (prev) { - const fullComponent = [model.get('project'), model.get('component')].join(' '); - const fullPrevComponent = [prev.get('project'), prev.get('component')].join(' '); - if (fullComponent !== fullPrevComponent) { - putComponent = true; - } - } - if (putComponent) { - this.displayComponent(container, model); - } - } - container.append(childView.el); - }, - - displayComponent(container, model) { - const data = { ...model.toJSON() }; - const qualifier = this.options.app.state.get('contextComponentQualifier'); - if (qualifier === 'VW' || qualifier === 'SVW') { - Object.assign(data, { organization: undefined }); - } else if (qualifier === 'TRK') { - Object.assign(data, { organization: undefined, project: undefined }); - } else if (qualifier === 'BRC') { - Object.assign(data, { organization: undefined, project: undefined, subProject: undefined }); - } else { - const organization = areThereCustomOrganizations() - ? getOrganization(model.get('projectOrganization')) - : null; - Object.assign(data, { organization }); - } - container.append(this.componentTemplate(data)); - }, - - destroyChildren() { - WorkspaceListView.prototype.destroyChildren.apply(this, arguments); - this.$('.issues-workspace-list-component').remove(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js index bf1b7d89862..1fba6f72119 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js @@ -51,7 +51,7 @@ class OrganizationFavoriteProjects extends React.Component { render() { return ( -
      +
      +
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "sinceLeakPeriod": "true", + "types": "BUG", + }, + } + }>
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "sinceLeakPeriod": "true", + "types": "VULNERABILITY", + }, + } + }>
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "sinceLeakPeriod": "true", + "types": "CODE_SMELL", + }, + } + }>
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "types": "BUG", + }, + } + }>
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "types": "VULNERABILITY", + }, + } + }>
      + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "types": "CODE_SMELL", + }, + } + }>
      - -
      - - {view === 'list' && - } - {view === 'list' && - } - {view === 'visualizations' && - } -
      -
      + + + + + + + {view === 'list' && + } + {view === 'list' && + } + {view === 'visualizations' && + } + + + ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js index c5c3b94bda1..69c18380bb3 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/App.js +++ b/server/sonar-web/src/main/js/apps/projects/components/App.js @@ -32,7 +32,7 @@ export default class App extends React.Component { render() { return ( -
      +
      {this.props.children}
      diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js index ed924559a85..d102a9dcef4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -55,14 +55,14 @@ export default class PageSidebar extends React.PureComponent { : undefined; return ( -
      +
      {isFiltered &&
      - {translate('projects.clear_all_filters')} + {translate('clear_all_filters')}
      } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js index 1c4cae090f2..d692e2ca9ce 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js @@ -19,9 +19,9 @@ */ import React from 'react'; import ProjectCardContainer from './ProjectCardContainer'; -import NoProjects from './NoProjects'; import NoFavoriteProjects from './NoFavoriteProjects'; import EmptyInstance from './EmptyInstance'; +import EmptySearch from '../../../components/common/EmptySearch'; export default class ProjectsList extends React.PureComponent { static propTypes = { @@ -37,7 +37,7 @@ export default class ProjectsList extends React.PureComponent { } else if (!this.props.isFiltered) { return ; } else { - return ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap index 211b0fbafb1..faf96ac7d0a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap @@ -16,7 +16,7 @@ exports[`test should handle \`view\` and \`visualization\` 2`] = ` }, } }> - projects.clear_all_filters + clear_all_filters
      `; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 45f8cd2663b..99e3232f7d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -10,14 +10,6 @@ margin-bottom: 0; } -.projects-empty-list { - padding: 60px 0; - border: 1px solid #e6e6e6; - border-radius: 2px; - text-align: center; - color: #777; -} - .project-card { position: relative; min-height: 121px; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js index 6d877d785fa..171d4ab8e0d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js @@ -21,7 +21,7 @@ import React from 'react'; import { Link } from 'react-router'; import ChangeProjectsView from '../views/ChangeProjectsView'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; import { getProfileProjects } from '../../../api/quality-profiles'; import { translate } from '../../../helpers/l10n'; import type { Profile } from '../propTypes'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js index 0acd2dc123d..41517e76d50 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js @@ -21,7 +21,6 @@ import { connect } from 'react-redux'; import SourceViewerBase from './SourceViewerBase'; import { receiveFavorites } from '../../store/favorites/duck'; -import { receiveIssues } from '../../store/issues/duck'; const mapStateToProps = null; @@ -39,11 +38,6 @@ const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean } }; -const onReceiveIssues = (issues: Array<*>) => - dispatch => { - dispatch(receiveIssues(issues)); - }; - -const mapDispatchToProps = { onReceiveComponent, onReceiveIssues }; +const mapDispatchToProps = { onReceiveComponent }; export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index c0cae1208f8..2e66388c3a1 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -70,10 +70,10 @@ type Props = { loadIssues: (string, number, number) => Promise<*>, loadSources: (string, number, number) => Promise<*>, onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, + onIssueChange?: (Issue) => void, onIssueSelect?: (string) => void, onIssueUnselect?: () => void, onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void, - onReceiveIssues: (issues: Array<*>) => void, selectedIssue?: string }; @@ -93,7 +93,7 @@ type State = { highlightedLine: number | null, highlightedSymbols: Array, issues?: Array, - issuesByLine: { [number]: Array }, + issuesByLine: { [number]: Array }, issueLocationsByLine: { [number]: Array }, 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) { const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; return ( @@ -561,6 +567,7 @@ export default class SourceViewerBase extends React.Component { loadingSourcesBefore={this.state.loadingSourcesBefore} onCoverageClick={this.handleCoverageClick} onDuplicationClick={this.handleDuplicationClick} + onIssueChange={this.handleIssueChange} onIssueSelect={this.handleIssueSelect} onIssueUnselect={this.handleIssueUnselect} onIssuesOpen={this.handleOpenIssues} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js index 8b9cfb46bd5..64aeedd5ba6 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js @@ -40,7 +40,7 @@ const ZERO_LINE = { }; export default class SourceViewerCode extends React.PureComponent { - props: { + props: {| displayAllIssues: boolean, duplications?: Array, duplicationsByLine: { [number]: Array }, @@ -51,7 +51,7 @@ export default class SourceViewerCode extends React.PureComponent { highlightedLine: number | null, highlightedSymbols: Array, issues: Array, - issuesByLine: { [number]: Array }, + issuesByLine: { [number]: Array }, issueLocationsByLine: { [number]: Array }, 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, symbolsByLine: { [number]: Array } - }; + |}; getDuplicationsForLine(line: SourceLine) { return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; } - getIssuesForLine(line: SourceLine): Array { + getIssuesForLine(line: SourceLine): Array { return this.props.issuesByLine[line.line] || EMPTY_ARRAY; } @@ -98,8 +99,11 @@ export default class SourceViewerCode extends React.PureComponent { } getSecondaryIssueLocationMessagesForLine(line: SourceLine, issueKey: string) { - return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] || - EMPTY_ARRAY; + const index = this.props.issueSecondaryLocationMessagesByIssueByLine; + if (index[issueKey] == null) { + return EMPTY_ARRAY; + } + return index[issueKey][line.line] || EMPTY_ARRAY; } renderLine = ( @@ -131,7 +135,8 @@ export default class SourceViewerCode extends React.PureComponent { optimizedHighlightedSymbols = EMPTY_ARRAY; } - const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) + const optimizedSelectedIssue = selectedIssue != null && + issuesForLine.find(issue => issue.key === selectedIssue) ? selectedIssue : null; @@ -165,6 +170,7 @@ export default class SourceViewerCode extends React.PureComponent { onClick={this.props.onLineClick} onCoverageClick={this.props.onCoverageClick} onDuplicationClick={this.props.onDuplicationClick} + onIssueChange={this.props.onIssueChange} onIssueSelect={this.props.onIssueSelect} onIssueUnselect={this.props.onIssueUnselect} onIssuesOpen={this.props.onIssuesOpen} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js index 4b65cd32ede..523ceeb192f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import { Link } from 'react-router'; -import QualifierIcon from '../shared/qualifier-icon'; +import QualifierIcon from '../shared/QualifierIcon'; import FavoriteContainer from '../controls/FavoriteContainer'; import { getProjectUrl, getIssuesUrl } from '../../helpers/urls'; import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; @@ -44,7 +44,8 @@ export default class SourceViewerHeader extends React.PureComponent { projectName: string, q: string, subProject?: string, - subProjectName?: string + subProjectName?: string, + uuid: string }, openNewWindow: () => void, showMeasures: () => void @@ -76,7 +77,8 @@ export default class SourceViewerHeader extends React.PureComponent { projectName, q, subProject, - subProjectName + subProjectName, + uuid } = this.props.component; const isUnitTest = q === 'UTS'; // TODO check if source viewer is displayed inside workspace @@ -169,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent {
      {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js index 74f3dabbfae..b1d051389a5 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js @@ -26,7 +26,7 @@ import LineSCM from './LineSCM'; import LineCoverage from './LineCoverage'; import LineDuplications from './LineDuplications'; import LineDuplicationBlock from './LineDuplicationBlock'; -import LineIssuesIndicatorContainer from './LineIssuesIndicatorContainer'; +import LineIssuesIndicator from './LineIssuesIndicator'; import LineCode from './LineCode'; import { TooltipsContainer } from '../../mixins/tooltips-mixin'; import type { SourceLine } from '../types'; @@ -35,8 +35,9 @@ import type { IndexedIssueLocation, IndexedIssueLocationMessage } from '../helpers/indexing'; +import type { Issue } from '../../issue/types'; -type Props = { +type Props = {| displayAllIssues: boolean, displayCoverage: boolean, displayDuplications: boolean, @@ -48,12 +49,13 @@ type Props = { highlighted: boolean, highlightedSymbols: Array, issueLocations: Array, - issues: Array, + issues: Array, 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, 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 && - } @@ -137,9 +139,10 @@ export default class Line extends React.PureComponent { , - issueKeys: Array, + issues: Array, issueLocations: Array, line: SourceLine, + onIssueChange: (Issue) => void, onIssueSelect: (issueKey: string) => void, onLocationSelect: (flowIndex: number, locationIndex: number) => void, onSymbolClick: (Array) => 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)}
      {showIssues && - issueKeys.length > 0 && + issues.length > 0 && } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js index daf1785ffd2..b7f1c2a176e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js @@ -23,9 +23,10 @@ import classNames from 'classnames'; import SeverityIcon from '../../shared/SeverityIcon'; import { sortBySeverity } from '../../../helpers/issues'; import type { SourceLine } from '../types'; +import type { Issue } from '../../issue/types'; type Props = { - issues: Array<{ severity: string }>, + issues: Array, line: SourceLine, onClick: () => void }; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js index ca89ab51fae..bff245af97c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js @@ -19,10 +19,12 @@ */ // @flow import React from 'react'; -import ConnectedIssue from '../../issue/ConnectedIssue'; +import Issue from '../../issue/Issue'; +import type { Issue as IssueType } from '../../issue/types'; type Props = { - issueKeys: Array, + issues: Array, + 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 (
      - {issueKeys.map(issueKey => ( - ( + ))}
      diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js index 3cc6793b214..5cd841a2a5a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js @@ -34,7 +34,7 @@ it('render code', () => { const wrapper = shallow( { const wrapper = shallow( { const line = { line: 3 }; - const issueKeys = ['foo', 'bar']; + const issues = [{ key: 'foo' }, { key: 'bar' }]; const onIssueClick = jest.fn(); const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap index 3e4499bb1bf..ca94ecedbf7 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap @@ -22,10 +22,14 @@ exports[`test render code 1`] = `
      - -
      diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js index 36bf7e73b3a..d39351c52dc 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js @@ -64,7 +64,7 @@ export const issuesByLine = (issues: Array) => { if (!(line in index)) { index[line] = []; } - index[line].push(issue.key); + index[line].push(issue); }); return index; }; diff --git a/server/sonar-web/src/main/js/components/__tests__/issue-test.js b/server/sonar-web/src/main/js/components/__tests__/issue-test.js deleted file mode 100644 index b0483b8cc21..00000000000 --- a/server/sonar-web/src/main/js/components/__tests__/issue-test.js +++ /dev/null @@ -1,197 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Issue from '../issue/models/issue'; - -describe('Model', () => { - it('should have correct urlRoot', () => { - const issue = new Issue(); - expect(issue.urlRoot()).toBe('/api/issues'); - }); - - it('should parse response without root issue object', () => { - const issue = new Issue(); - const example = { a: 1 }; - expect(issue.parse(example)).toEqual(example); - }); - - it('should parse response with the root issue object', () => { - const issue = new Issue(); - const example = { a: 1 }; - expect(issue.parse({ issue: example })).toEqual(example); - }); - - it('should reset attributes (no attributes initially)', () => { - const issue = new Issue(); - const example = { a: 1 }; - issue.reset(example); - expect(issue.toJSON()).toEqual(example); - }); - - it('should reset attributes (override attribute)', () => { - const issue = new Issue({ a: 2 }); - const example = { a: 1 }; - issue.reset(example); - expect(issue.toJSON()).toEqual(example); - }); - - it('should reset attributes (different attributes)', () => { - const issue = new Issue({ a: 2 }); - const example = { b: 1 }; - issue.reset(example); - expect(issue.toJSON()).toEqual(example); - }); - - it('should unset `textRange` of a closed issue', () => { - const issue = new Issue(); - const result = issue.parse({ issue: { status: 'CLOSED', textRange: { startLine: 5 } } }); - expect(result.textRange).toBeFalsy(); - }); - - it('should unset `flows` of a closed issue', () => { - const issue = new Issue(); - const result = issue.parse({ issue: { status: 'CLOSED', flows: [1, 2, 3] } }); - expect(result.flows).toEqual([]); - }); - - describe('Actions', () => { - it('should assign', () => { - const issue = new Issue({ key: 'issue-key' }); - const spy = jest.fn(); - issue._action = spy; - issue.assign('admin'); - expect(spy).toBeCalledWith({ - data: { assignee: 'admin', issue: 'issue-key' }, - url: '/api/issues/assign' - }); - }); - - it('should unassign', () => { - const issue = new Issue({ key: 'issue-key' }); - const spy = jest.fn(); - issue._action = spy; - issue.assign(); - expect(spy).toBeCalledWith({ - data: { assignee: undefined, issue: 'issue-key' }, - url: '/api/issues/assign' - }); - }); - - it('should plan', () => { - const issue = new Issue({ key: 'issue-key' }); - const spy = jest.fn(); - issue._action = spy; - issue.plan('plan'); - expect(spy).toBeCalledWith({ - data: { plan: 'plan', issue: 'issue-key' }, - url: '/api/issues/plan' - }); - }); - - it('should unplan', () => { - const issue = new Issue({ key: 'issue-key' }); - const spy = jest.fn(); - issue._action = spy; - issue.plan(); - expect(spy).toBeCalledWith({ - data: { plan: undefined, issue: 'issue-key' }, - url: '/api/issues/plan' - }); - }); - - it('should set severity', () => { - const issue = new Issue({ key: 'issue-key' }); - const spy = jest.fn(); - issue._action = spy; - issue.setSeverity('BLOCKER'); - expect(spy).toBeCalledWith({ - data: { severity: 'BLOCKER', issue: 'issue-key' }, - url: '/api/issues/set_severity' - }); - }); - }); - - describe('#getLinearLocations', () => { - it('should return single line location', () => { - const issue = new Issue({ - textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 10 } - }); - const locations = issue.getLinearLocations(); - expect(locations.length).toBe(1); - - expect(locations[0].line).toBe(1); - expect(locations[0].from).toBe(0); - expect(locations[0].to).toBe(10); - }); - - it('should return location not from 0', () => { - const issue = new Issue({ - textRange: { startLine: 1, endLine: 1, startOffset: 5, endOffset: 10 } - }); - const locations = issue.getLinearLocations(); - expect(locations.length).toBe(1); - - expect(locations[0].line).toBe(1); - expect(locations[0].from).toBe(5); - expect(locations[0].to).toBe(10); - }); - - it('should return 2-lines location', () => { - const issue = new Issue({ - textRange: { startLine: 2, endLine: 3, startOffset: 5, endOffset: 10 } - }); - const locations = issue.getLinearLocations(); - expect(locations.length).toBe(2); - - expect(locations[0].line).toBe(2); - expect(locations[0].from).toBe(5); - expect(locations[0].to).toBe(999999); - - expect(locations[1].line).toBe(3); - expect(locations[1].from).toBe(0); - expect(locations[1].to).toBe(10); - }); - - it('should return 3-lines location', () => { - const issue = new Issue({ - textRange: { startLine: 4, endLine: 6, startOffset: 5, endOffset: 10 } - }); - const locations = issue.getLinearLocations(); - expect(locations.length).toBe(3); - - expect(locations[0].line).toBe(4); - expect(locations[0].from).toBe(5); - expect(locations[0].to).toBe(999999); - - expect(locations[1].line).toBe(5); - expect(locations[1].from).toBe(0); - expect(locations[1].to).toBe(999999); - - expect(locations[2].line).toBe(6); - expect(locations[2].from).toBe(0); - expect(locations[2].to).toBe(10); - }); - - it('should return [] when no location', () => { - const issue = new Issue(); - const locations = issue.getLinearLocations(); - expect(locations.length).toBe(0); - }); - }); -}); diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js index dc4f0082f27..4e1d336dc0d 100644 --- a/server/sonar-web/src/main/js/components/charts/bar-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -21,7 +21,7 @@ import React from 'react'; import { max } from 'd3-array'; import { scaleLinear, scaleBand } from 'd3-scale'; import { ResizeMixin } from './../mixins/resize-mixin'; -import { TooltipsMixin } from './../mixins/tooltips-mixin'; +import { TooltipsContainer } from './../mixins/tooltips-mixin'; export const BarChart = React.createClass({ propTypes: { @@ -34,7 +34,7 @@ export const BarChart = React.createClass({ onBarClick: React.PropTypes.func }, - mixins: [ResizeMixin, TooltipsMixin], + mixins: [ResizeMixin], getDefaultProps() { return { @@ -162,13 +162,15 @@ export const BarChart = React.createClass({ const yScale = scaleLinear().domain([0, maxY]).range([availableHeight, 0]); return ( - - - {this.renderXTicks(xScale, yScale)} - {this.renderXValues(xScale, yScale)} - {this.renderBars(xScale, yScale)} - - + + + + {this.renderXTicks(xScale, yScale)} + {this.renderXValues(xScale, yScale)} + {this.renderBars(xScale, yScale)} + + + ); } }); diff --git a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js index d91207af777..7b06317b39e 100644 --- a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js +++ b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import QualifierIcon from '../shared/qualifier-icon'; +import QualifierIcon from '../shared/QualifierIcon'; export const TreemapBreadcrumbs = React.createClass({ propTypes: { diff --git a/server/sonar-web/src/main/js/components/common/EmptySearch.js b/server/sonar-web/src/main/js/components/common/EmptySearch.js new file mode 100644 index 00000000000..904a6b2cbad --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/EmptySearch.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css } from 'glamor'; +import { translate } from '../../helpers/l10n'; + +const EmptySearch = () => ( +
      +

      {translate('no_results_search')}

      +

      {translate('no_results_search.2')}

      +
      +); + +export default EmptySearch; diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js index 2d83b6aeb24..8c5db3a8fe1 100644 --- a/server/sonar-web/src/main/js/components/common/MarkdownTips.js +++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.js @@ -25,7 +25,7 @@ import { translate } from '../../helpers/l10n'; export default class MarkdownTips extends React.PureComponent { handleClick(evt: MouseEvent) { evt.preventDefault(); - window.open(getMarkdownHelpUrl(), 'height=300,width=600,scrollbars=1,resizable=1'); + window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1'); } render() { diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js index ba2f82b34b7..bec5c2e6712 100644 --- a/server/sonar-web/src/main/js/components/common/SelectList.js +++ b/server/sonar-web/src/main/js/components/common/SelectList.js @@ -19,6 +19,8 @@ */ // @flow import React from 'react'; +import key from 'keymaster'; +import { uniqueId } from 'lodash'; import SelectListItem from './SelectListItem'; type Props = { @@ -33,7 +35,8 @@ type State = { }; export default class SelectList extends React.PureComponent { - list: HTMLElement; + currentKeyScope: string; + previousKeyScope: string; props: Props; state: State; @@ -45,7 +48,7 @@ export default class SelectList extends React.PureComponent { } componentDidMount() { - this.list.focus(); + this.attachShortcuts(); } componentWillReceiveProps(nextProps: Props) { @@ -57,24 +60,36 @@ export default class SelectList extends React.PureComponent { } } - handleKeyboard = (evt: KeyboardEvent) => { - switch (evt.keyCode) { - case 40: // down - this.setState(this.selectNextElement); - break; - case 38: // up - this.setState(this.selectPreviousElement); - break; - case 13: // return - if (this.state.active) { - this.handleSelect(this.state.active); - } - break; - default: - return; - } - evt.preventDefault(); - evt.stopPropagation(); + componentWillUnmount() { + this.detachShortcuts(); + } + + attachShortcuts = () => { + this.previousKeyScope = key.getScope(); + this.currentKeyScope = uniqueId('key-scope'); + key.setScope(this.currentKeyScope); + + key('down', this.currentKeyScope, () => { + this.setState(this.selectNextElement); + return false; + }); + + key('up', this.currentKeyScope, () => { + this.setState(this.selectPreviousElement); + return false; + }); + + key('return', this.currentKeyScope, () => { + if (this.state.active) { + this.handleSelect(this.state.active); + } + return false; + }); + }; + + detachShortcuts = () => { + key.setScope(this.previousKeyScope); + key.deleteScope(this.currentKeyScope); }; handleSelect = (item: string) => { @@ -105,18 +120,18 @@ export default class SelectList extends React.PureComponent { const { children } = this.props; const hasChildren = React.Children.count(children) > 0; return ( -
        this.list = list} - tabIndex={0}> +
          {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 => ( { ))} ); - keydown(list.find('ul'), 40); + keydown(40); expect(list.state()).toMatchSnapshot(); - keydown(list.find('ul'), 40); + keydown(40); expect(list.state()).toMatchSnapshot(); - keydown(list.find('ul'), 38); + keydown(38); expect(list.state()).toMatchSnapshot(); click(list.childAt(2).find('a')); expect(onSelect.mock.calls).toMatchSnapshot(); // eslint-disable-linelist diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap index 4cf15f469cb..b2d9388c7ef 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap @@ -26,9 +26,7 @@ Array [ exports[`test should render correctly with children 1`] = `
            + className="menu"> + className="menu"> + diff --git a/server/sonar-web/src/main/js/components/issue/BaseIssue.js b/server/sonar-web/src/main/js/components/issue/BaseIssue.js deleted file mode 100644 index d4ada02869b..00000000000 --- a/server/sonar-web/src/main/js/components/issue/BaseIssue.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import IssueView from './IssueView'; -import { setIssueAssignee } from '../../api/issues'; -import type { Issue } from './types'; - -type Props = { - checked?: boolean, - issue: Issue, - onCheck?: () => void, - onClick: (string) => void, - onFail: (Error) => void, - onFilterClick?: () => void, - onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, - selected: boolean -}; - -type State = { - currentPopup: string -}; - -export default class BaseIssue extends React.PureComponent { - mounted: boolean; - props: Props; - state: State; - - static defaultProps = { - selected: false - }; - - constructor(props: Props) { - super(props); - this.state = { - currentPopup: '' - }; - } - - componentDidMount() { - this.mounted = true; - if (this.props.selected) { - this.bindShortcuts(); - } - } - - componentWillUpdate(nextProps: Props) { - if (!nextProps.selected && this.props.selected) { - this.unbindShortcuts(); - } - } - - componentDidUpdate(prevProps: Props) { - if (!prevProps.selected && this.props.selected) { - this.bindShortcuts(); - } - } - - componentWillUnmount() { - this.mounted = false; - if (this.props.selected) { - this.unbindShortcuts(); - } - } - - bindShortcuts() { - document.addEventListener('keypress', this.handleKeyPress); - } - - unbindShortcuts() { - document.removeEventListener('keypress', this.handleKeyPress); - } - - togglePopup = (popupName: string, open?: boolean) => { - if (this.mounted) { - this.setState((prevState: State) => { - if (prevState.currentPopup !== popupName && open !== false) { - return { currentPopup: popupName }; - } else if (prevState.currentPopup === popupName && open !== true) { - return { currentPopup: '' }; - } - return prevState; - }); - } - }; - - handleAssignement = (login: string) => { - const { issue } = this.props; - if (issue.assignee !== login) { - this.props.onIssueChange(setIssueAssignee({ issue: issue.key, assignee: login })); - } - this.togglePopup('assign', false); - }; - - handleKeyPress = (e: Object) => { - const tagName = e.target.tagName.toUpperCase(); - const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; - - if (shouldHandle) { - switch (e.key) { - case 'f': - return this.togglePopup('transition'); - case 'a': - return this.togglePopup('assign'); - case 'm': - return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me'); - case 'p': - return this.togglePopup('plan'); - case 'i': - return this.togglePopup('set-severity'); - case 'c': - return this.togglePopup('comment'); - case 't': - return this.togglePopup('edit-tags'); - } - } - }; - - render() { - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js index a121bf738d0..471a62e04f0 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.js +++ b/server/sonar-web/src/main/js/components/issue/Issue.js @@ -18,14 +18,145 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import { connect } from 'react-redux'; -import BaseIssue from './BaseIssue'; -import { onFail } from '../../store/rootActions'; +import React from 'react'; +import IssueView from './IssueView'; import { updateIssue } from './actions'; +import { setIssueAssignee } from '../../api/issues'; +import { onFail } from '../../store/rootActions'; +import type { Issue } from './types'; + +type Props = {| + checked?: boolean, + issue: Issue, + onChange: (Issue) => void, + onCheck?: (string) => void, + onClick: (string) => void, + onFilter?: (property: string, issue: Issue) => void, + selected: boolean +|}; -const mapDispatchToProps = { - onIssueChange: updateIssue, - onFail: error => dispatch => onFail(dispatch)(error) +type State = { + currentPopup: string }; -export default connect(null, mapDispatchToProps)(BaseIssue); +export default class BaseIssue extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + static contextTypes = { + store: React.PropTypes.object + }; + + static defaultProps = { + selected: false + }; + + constructor(props: Props) { + super(props); + this.state = { + currentPopup: '' + }; + } + + componentDidMount() { + this.mounted = true; + if (this.props.selected) { + this.bindShortcuts(); + } + } + + componentWillUpdate(nextProps: Props) { + if (!nextProps.selected && this.props.selected) { + this.unbindShortcuts(); + } + } + + componentDidUpdate(prevProps: Props) { + if (!prevProps.selected && this.props.selected) { + this.bindShortcuts(); + } + } + + componentWillUnmount() { + this.mounted = false; + if (this.props.selected) { + this.unbindShortcuts(); + } + } + + bindShortcuts() { + document.addEventListener('keypress', this.handleKeyPress); + } + + unbindShortcuts() { + document.removeEventListener('keypress', this.handleKeyPress); + } + + togglePopup = (popupName: string, open?: boolean) => { + if (this.mounted) { + this.setState((prevState: State) => { + if (prevState.currentPopup !== popupName && open !== false) { + return { currentPopup: popupName }; + } else if (prevState.currentPopup === popupName && open !== true) { + return { currentPopup: '' }; + } + return prevState; + }); + } + }; + + handleAssignement = (login: string) => { + const { issue } = this.props; + if (issue.assignee !== login) { + updateIssue(this.props.onChange, setIssueAssignee({ issue: issue.key, assignee: login })); + } + this.togglePopup('assign', false); + }; + + handleFail = (error: Error) => { + onFail(this.context.store.dispatch)(error); + }; + + handleKeyPress = (e: Object) => { + const tagName = e.target.tagName.toUpperCase(); + const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; + + if (shouldHandle) { + switch (e.key) { + case 'f': + return this.togglePopup('transition'); + case 'a': + return this.togglePopup('assign'); + case 'm': + return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me'); + case 'p': + return this.togglePopup('plan'); + case 'i': + return this.togglePopup('set-severity'); + case 'c': + return this.togglePopup('comment'); + case 't': + return this.togglePopup('edit-tags'); + } + } + }; + + render() { + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js index 52ee7e95280..3d959f26573 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.js +++ b/server/sonar-web/src/main/js/components/issue/IssueView.js @@ -20,43 +20,51 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import Checkbox from '../../components/controls/Checkbox'; import IssueTitleBar from './components/IssueTitleBar'; import IssueActionsBar from './components/IssueActionsBar'; import IssueCommentLine from './components/IssueCommentLine'; +import { updateIssue } from './actions'; import { deleteIssueComment, editIssueComment } from '../../api/issues'; import type { Issue } from './types'; -type Props = { +type Props = {| checked?: boolean, currentPopup: string, issue: Issue, onAssign: (string) => void, - onCheck?: () => void, + onChange: (Issue) => void, + onCheck?: (string) => void, onClick: (string) => void, onFail: (Error) => void, - onFilterClick?: () => void, - onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, + onFilter?: (property: string, issue: Issue) => void, selected: boolean, togglePopup: (string) => void -}; +|}; export default class IssueView extends React.PureComponent { props: Props; - handleClick = (evt: MouseEvent) => { - evt.preventDefault(); + handleCheck = (event: Event) => { + event.preventDefault(); + event.stopPropagation(); + if (this.props.onCheck) { + this.props.onCheck(this.props.issue.key); + } + }; + + handleClick = (event: Event & { target: HTMLElement }) => { + event.preventDefault(); if (this.props.onClick) { this.props.onClick(this.props.issue.key); } }; editComment = (comment: string, text: string) => { - this.props.onIssueChange(editIssueComment({ comment, text })); + updateIssue(this.props.onChange, editIssueComment({ comment, text })); }; deleteComment = (comment: string) => { - this.props.onIssueChange(deleteIssueComment({ comment })); + updateIssue(this.props.onChange, deleteIssueComment({ comment })); }; render() { @@ -74,13 +82,13 @@ export default class IssueView extends React.PureComponent { className={issueClass} data-issue={issue.key} onClick={this.handleClick} - tabIndex={0} - role="listitem"> + role="listitem" + tabIndex={0}> {issue.comments && issue.comments.length > 0 && @@ -108,13 +116,13 @@ export default class IssueView extends React.PureComponent { {hasCheckbox && -
            - + -
            } + }
      ); } diff --git a/server/sonar-web/src/main/js/components/issue/actions.js b/server/sonar-web/src/main/js/components/issue/actions.js index a0631c17001..a44430520bd 100644 --- a/server/sonar-web/src/main/js/components/issue/actions.js +++ b/server/sonar-web/src/main/js/components/issue/actions.js @@ -18,35 +18,41 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import type { Dispatch } from 'redux'; -import type { Issue } from './types'; import { onFail } from '../../store/rootActions'; -import { receiveIssues } from '../../store/issues/duck'; import { parseIssueFromResponse } from '../../helpers/issues'; +import type { Issue } from './types'; -export const updateIssue = (resultPromise: Promise<*>, oldIssue?: Issue, newIssue?: Issue) => - (dispatch: Dispatch<*>) => { - if (oldIssue && newIssue) { - dispatch(receiveIssues([newIssue])); - } - resultPromise.then( - response => { - dispatch( - receiveIssues([ - parseIssueFromResponse( - response.issue, - response.components, - response.users, - response.rules - ) - ]) +export const updateIssue = ( + onChange: (Issue) => void, + resultPromise: Promise<*>, + oldIssue?: Issue, + newIssue?: Issue +) => { + const optimisticUpdate = oldIssue != null && newIssue != null; + + if (optimisticUpdate) { + // $FlowFixMe `newIssue` is not null, because `optimisticUpdate` is true + onChange(newIssue); + } + + resultPromise.then( + response => { + if (!optimisticUpdate) { + const issue = parseIssueFromResponse( + response.issue, + response.components, + response.users, + response.rules ); - }, - error => { - onFail(dispatch)(error); - if (oldIssue && newIssue) { - dispatch(receiveIssues([oldIssue])); - } + onChange(issue); + } + }, + error => { + onFail(error); + if (optimisticUpdate) { + // $FlowFixMe `oldIssue` is not null, because `optimisticUpdate` is true + onChange(oldIssue); } - ); - }; + } + ); +}; diff --git a/server/sonar-web/src/main/js/components/issue/collections/issues.js b/server/sonar-web/src/main/js/components/issue/collections/issues.js deleted file mode 100644 index 69ac37b1beb..00000000000 --- a/server/sonar-web/src/main/js/components/issue/collections/issues.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Backbone from 'backbone'; -import Issue from '../models/issue'; - -export default Backbone.Collection.extend({ - model: Issue, - - url() { - return window.baseUrl + '/api/issues/search'; - }, - - _injectRelational(issue, source, baseField, lookupField) { - const baseValue = issue[baseField]; - if (baseValue != null && Array.isArray(source) && source.length > 0) { - const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); - if (lookupValue != null) { - Object.keys(lookupValue).forEach(key => { - const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1); - issue[newKey] = lookupValue[key]; - }); - } - } - return issue; - }, - - _injectCommentsRelational(issue, users) { - if (issue.comments) { - const that = this; - const newComments = issue.comments.map(comment => { - let newComment = { ...comment, author: comment.login }; - delete newComment.login; - newComment = that._injectRelational(newComment, users, 'author', 'login'); - return newComment; - }); - issue = { ...issue, comments: newComments }; - } - return issue; - }, - - _prepareClosed(issue) { - if (issue.status === 'CLOSED') { - issue.flows = []; - delete issue.textRange; - } - return issue; - }, - - ensureTextRange(issue) { - if (issue.line && !issue.textRange) { - // FIXME 999999 - issue.textRange = { - startLine: issue.line, - endLine: issue.line, - startOffset: 0, - endOffset: 999999 - }; - } - return issue; - }, - - parse(r) { - const that = this; - - this.paging = { - p: r.p, - ps: r.ps, - total: r.total, - maxResultsReached: r.p * r.ps >= r.total - }; - - return r.issues.map(issue => { - issue = that._injectRelational(issue, r.components, 'component', 'key'); - issue = that._injectRelational(issue, r.components, 'project', 'key'); - issue = that._injectRelational(issue, r.components, 'subProject', 'key'); - issue = that._injectRelational(issue, r.rules, 'rule', 'key'); - issue = that._injectRelational(issue, r.users, 'assignee', 'login'); - issue = that._injectCommentsRelational(issue, r.users); - issue = that._prepareClosed(issue); - issue = that.ensureTextRange(issue); - return issue; - }); - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js index e60bc87c991..9006cd9a19e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js @@ -25,6 +25,7 @@ import IssueSeverity from './IssueSeverity'; import IssueTags from './IssueTags'; import IssueTransition from './IssueTransition'; import IssueType from './IssueType'; +import { updateIssue } from '../actions'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import type { Issue } from '../types'; @@ -32,8 +33,8 @@ type Props = { issue: Issue, currentPopup: string, onAssign: (string) => void, + onChange: (Issue) => void, onFail: (Error) => void, - onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, togglePopup: (string) => void }; @@ -63,15 +64,18 @@ export default class IssueActionsBar extends React.PureComponent { const { issue } = this.props; if (issue[property] !== value) { const newIssue = { ...issue, [property]: value }; - this.props.onIssueChange(apiCall({ issue: issue.key, [property]: value }), issue, newIssue); + updateIssue( + this.props.onChange, + apiCall({ issue: issue.key, [property]: value }), + issue, + newIssue + ); } this.props.togglePopup(popup, false); }; toggleComment = (open?: boolean, placeholder?: string) => { - this.setState({ - commentPlaceholder: placeholder || '' - }); + this.setState({ commentPlaceholder: placeholder || '' }); this.props.togglePopup('comment', open); }; @@ -112,8 +116,8 @@ export default class IssueActionsBar extends React.PureComponent { isOpen={this.props.currentPopup === 'transition' && hasTransitions} issue={issue} hasTransitions={hasTransitions} + onChange={this.props.onChange} togglePopup={this.props.togglePopup} - setIssueProperty={this.setIssueProperty} />
    • @@ -134,10 +138,10 @@ export default class IssueActionsBar extends React.PureComponent {
    • } {canComment && }
    @@ -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} />
  • diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js index f5f6bf5b8d3..8111815e942 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js @@ -19,25 +19,26 @@ */ // @flow import React from 'react'; +import { updateIssue } from '../actions'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import CommentPopup from '../popups/CommentPopup'; import { addIssueComment } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; import type { Issue } from '../types'; -type Props = { - issueKey: string, +type Props = {| commentPlaceholder: string, currentPopup: string, - onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, + issueKey: string, + onChange: (Issue) => void, toggleComment: (open?: boolean, placeholder?: string) => void -}; +|}; export default class IssueCommentAction extends React.PureComponent { props: Props; addComment = (text: string) => { - this.props.onIssueChange(addIssueComment({ issue: this.props.issueKey, text })); + updateIssue(this.props.onChange, addIssueComment({ issue: this.props.issueKey, text })); this.props.toggleComment(false); }; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js index ab850061b7d..c7cebdf74d7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import { updateIssue } from '../actions'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetIssueTagsPopup from '../popups/SetIssueTagsPopup'; import TagsList from '../../../components/tags/TagsList'; @@ -26,14 +27,14 @@ import { setIssueTags } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; import type { Issue } from '../types'; -type Props = { +type Props = {| canSetTags: boolean, isOpen: boolean, issue: Issue, + onChange: (Issue) => void, onFail: (Error) => void, - onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, togglePopup: (string) => void -}; +|}; export default class IssueTags extends React.PureComponent { props: Props; @@ -45,7 +46,8 @@ export default class IssueTags extends React.PureComponent { setTags = (tags: Array) => { const { issue } = this.props; const newIssue = { ...issue, tags }; - this.props.onIssueChange( + updateIssue( + this.props.onChange, setIssueTags({ issue: issue.key, tags: tags.join(',') }), issue, newIssue diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js index 4f847049f54..55ef295f55d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -19,23 +19,27 @@ */ // @flow import React from 'react'; +import { Link } from 'react-router'; import IssueChangelog from './IssueChangelog'; import IssueMessage from './IssueMessage'; +import SimilarIssuesFilter from './SimilarIssuesFilter'; import { getSingleIssueUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; import type { Issue } from '../types'; -type Props = { +type Props = {| issue: Issue, currentPopup: string, onFail: (Error) => void, - onFilterClick?: () => void, + onFilter?: (property: string, issue: Issue) => void, togglePopup: (string) => void -}; +|}; + +const stopPropagation = (event: Event) => event.stopPropagation(); export default function IssueTitleBar(props: Props) { const { issue } = props; - const hasSimilarIssuesFilter = props.onFilterClick != null; + const hasSimilarIssuesFilter = props.onFilter != null; return ( @@ -66,21 +70,21 @@ export default function IssueTitleBar(props: Props) { }
  • -
  • {hasSimilarIssuesFilter &&
  • - +
  • } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js index 03cd4e41d86..24e3625d529 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import { updateIssue } from '../actions'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import SetTransitionPopup from '../popups/SetTransitionPopup'; import StatusHelper from '../../../components/shared/StatusHelper'; @@ -29,15 +30,20 @@ type Props = { hasTransitions: boolean, isOpen: boolean, issue: Issue, - setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void, + onChange: (Issue) => void, togglePopup: (string) => void }; export default class IssueTransition extends React.PureComponent { props: Props; - setTransition = (transition: string) => - this.props.setIssueProperty('transition', 'transition', setIssueTransition, transition); + setTransition = (transition: string) => { + updateIssue( + this.props.onChange, + setIssueTransition({ issue: this.props.issue.key, transition }) + ); + this.toggleSetTransition(); + }; toggleSetTransition = (open?: boolean) => { this.props.togglePopup('transition', open); diff --git a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js new file mode 100644 index 00000000000..c28593d7c89 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; +import SimilarIssuesPopup from '../popups/SimilarIssuesPopup'; +import { translate } from '../../../helpers/l10n'; +import type { Issue } from '../types'; + +type Props = {| + isOpen: boolean, + issue: Issue, + togglePopup: (string) => void, + onFail: (Error) => void, + onFilter: (property: string, issue: Issue) => void +|}; + +export default class SimilarIssuesFilter extends React.PureComponent { + props: Props; + + handleClick = (evt: SyntheticInputEvent) => { + evt.preventDefault(); + this.togglePopup(); + }; + + handleFilter = (property: string, issue: Issue) => { + this.togglePopup(false); + this.props.onFilter(property, issue); + }; + + togglePopup = (open?: boolean) => { + this.props.togglePopup('similarIssues', open); + }; + + render() { + return ( + }> + + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js index ca4a95ff08b..608112423d5 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -19,7 +19,6 @@ */ import { shallow } from 'enzyme'; import React from 'react'; -import moment from 'moment'; import IssueChangelog from '../IssueChangelog'; import { click } from '../../../../helpers/testUtils'; @@ -29,7 +28,11 @@ const issue = { creationDate: '2017-03-01T09:36:01+0100' }; -moment.fn.fromNow = jest.fn(() => 'a month ago'); +jest.mock('moment', () => + () => ({ + format: () => 'March 1, 2017 9:36 AM', + fromNow: () => 'a month ago' + })); it('should render correctly', () => { const element = shallow( diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js index d681183f2c3..9096b729386 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js @@ -19,7 +19,6 @@ */ import { shallow } from 'enzyme'; import React from 'react'; -import moment from 'moment'; import IssueCommentLine from '../IssueCommentLine'; import { click } from '../../../../helpers/testUtils'; @@ -32,7 +31,7 @@ const comment = { updatable: true }; -moment.fn.fromNow = jest.fn(() => 'a month ago'); +jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' })); it('should render correctly a comment that is not updatable', () => { const element = shallow( diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js index 3e110b92f36..1d3b7ac4e0e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js @@ -43,7 +43,7 @@ it('should render the titlebar with the filter', () => { issue={issue} currentPopup="" onFail={jest.fn()} - onFilterClick={jest.fn()} + onFilter={jest.fn()} togglePopup={jest.fn()} /> ); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap index f51811bbd0f..e00752935ca 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -42,10 +42,19 @@ exports[`test should render the titlebar correctly 1`] = `
  • - + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/issues", + "query": Object { + "issues": "AVsae-CQS-9G3txfbFN2", + }, + } + } />
  • @@ -98,23 +107,37 @@ exports[`test should render the titlebar with the filter 1`] = `
  • - + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/issues", + "query": Object { + "issues": "AVsae-CQS-9G3txfbFN2", + }, + } + } />
  • - +
  • diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js deleted file mode 100644 index e9b4c47cfcd..00000000000 --- a/server/sonar-web/src/main/js/components/issue/issue-view.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import Backbone from 'backbone'; -import Marionette from 'backbone.marionette'; -import ChangeLog from './models/changelog'; -import ChangeLogView from './views/changelog-view'; -import TransitionsFormView from './views/transitions-form-view'; -import AssignFormView from './views/assign-form-view'; -import CommentFormView from './views/comment-form-view'; -import DeleteCommentView from './views/DeleteCommentView'; -import SetSeverityFormView from './views/set-severity-form-view'; -import SetTypeFormView from './views/set-type-form-view'; -import TagsFormView from './views/tags-form-view'; -import Template from './templates/issue.hbs'; -import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; - -export default Marionette.ItemView.extend({ - template: Template, - - modelEvents: { - change: 'notifyAndRender', - transition: 'onTransition' - }, - - className() { - const hasCheckbox = this.options.onCheck != null; - return hasCheckbox ? 'issue issue-with-checkbox' : 'issue'; - }, - - events() { - return { - click: 'handleClick', - 'click .js-issue-comment': 'onComment', - 'click .js-issue-comment-edit': 'editComment', - 'click .js-issue-comment-delete': 'deleteComment', - 'click .js-issue-transition': 'transition', - 'click .js-issue-set-severity': 'setSeverity', - 'click .js-issue-set-type': 'setType', - 'click .js-issue-assign': 'assign', - 'click .js-issue-assign-to-me': 'assignToMe', - 'click .js-issue-plan': 'plan', - 'click .js-issue-show-changelog': 'showChangeLog', - 'click .js-issue-rule': 'showRule', - 'click .js-issue-edit-tags': 'editTags', - 'click .js-issue-locations': 'showLocations', - 'click .js-issue-filter': 'filterSimilarIssues', - 'click .js-toggle': 'onIssueCheck', - 'click .js-issue-permalink': 'onPermalinkClick' - }; - }, - - notifyAndRender() { - const { onIssueChange } = this.options; - if (onIssueChange) { - onIssueChange(this.model.toJSON()); - } - - // if ConnectedIssue is used, this view can be destroyed just after onIssueChange() - if (!this.isDestroyed) { - this.render(); - } - }, - - onRender() { - this.$el.attr('data-key', this.model.get('key')); - }, - - disableControls() { - this.$(':input').prop('disabled', true); - }, - - enableControls() { - this.$(':input').prop('disabled', false); - }, - - resetIssue(options) { - const that = this; - const key = this.model.get('key'); - const componentUuid = this.model.get('componentUuid'); - this.model.reset({ key, componentUuid }, { silent: true }); - return this.model.fetch(options).done(() => that.trigger('reset')); - }, - - showChangeLog(e) { - e.preventDefault(); - e.stopPropagation(); - const that = this; - const t = $(e.currentTarget); - const changeLog = new ChangeLog(); - return changeLog - .fetch({ - data: { issue: this.model.get('key') } - }) - .done(() => { - if (that.popup) { - that.popup.destroy(); - } - that.popup = new ChangeLogView({ - triggerEl: t, - bottomRight: true, - collection: changeLog, - issue: that.model - }); - that.popup.render(); - }); - }, - - updateAfterAction(response) { - if (this.popup) { - this.popup.destroy(); - } - if (response) { - this.model.set(this.model.parse(response)); - } - }, - - onComment(e) { - e.stopPropagation(); - this.comment(); - }, - - comment(options) { - $('body').click(); - this.popup = new CommentFormView({ - triggerEl: this.$('.js-issue-comment'), - bottom: true, - issue: this.model, - detailView: this, - additionalOptions: options - }); - this.popup.render(); - }, - - editComment(e) { - e.stopPropagation(); - $('body').click(); - const commentEl = $(e.currentTarget).closest('.issue-comment'); - const commentKey = commentEl.data('comment-key'); - const comment = this.model.get('comments').find(comment => comment.key === commentKey); - this.popup = new CommentFormView({ - triggerEl: $(e.currentTarget), - bottomRight: true, - model: new Backbone.Model(comment), - issue: this.model, - detailView: this - }); - this.popup.render(); - }, - - deleteComment(e) { - e.stopPropagation(); - $('body').click(); - const commentEl = $(e.currentTarget).closest('.issue-comment'); - const commentKey = commentEl.data('comment-key'); - this.popup = new DeleteCommentView({ - triggerEl: $(e.currentTarget), - bottomRight: true, - onDelete: () => { - this.disableControls(); - $.ajax({ - type: 'POST', - url: window.baseUrl + '/api/issues/delete_comment?key=' + commentKey - }).done(r => this.updateAfterAction(r)); - } - }); - this.popup.render(); - }, - - transition(e) { - e.stopPropagation(); - $('body').click(); - this.popup = new TransitionsFormView({ - triggerEl: $(e.currentTarget), - bottom: true, - model: this.model, - view: this - }); - this.popup.render(); - }, - - setSeverity(e) { - e.stopPropagation(); - $('body').click(); - this.popup = new SetSeverityFormView({ - triggerEl: $(e.currentTarget), - bottom: true, - model: this.model - }); - this.popup.render(); - }, - - setType(e) { - e.stopPropagation(); - $('body').click(); - this.popup = new SetTypeFormView({ - triggerEl: $(e.currentTarget), - bottom: true, - model: this.model - }); - this.popup.render(); - }, - - assign(e) { - e.stopPropagation(); - $('body').click(); - this.popup = new AssignFormView({ - triggerEl: $(e.currentTarget), - bottom: true, - model: this.model - }); - this.popup.render(); - }, - - assignToMe() { - const view = new AssignFormView({ - model: this.model, - triggerEl: $('body') - }); - const currentUser = getCurrentUserFromStore(); - view.submit(currentUser.login, currentUser.name); - view.destroy(); - }, - - showRule(e) { - e.preventDefault(); - e.stopPropagation(); - const ruleKey = this.model.get('rule'); - // lazy load Workspace - const Workspace = require('../workspace/main').default; - Workspace.openRule({ key: ruleKey, organization: this.model.get('projectOrganization') }); - }, - - action(action) { - this.disableControls(); - return this.model - .customAction(action) - .done(r => this.updateAfterAction(r)) - .fail(() => this.enableControls()); - }, - - editTags(e) { - e.stopPropagation(); - $('body').click(); - this.popup = new TagsFormView({ - triggerEl: $(e.currentTarget), - bottomRight: true, - model: this.model - }); - this.popup.render(); - }, - - showLocations() { - this.model.trigger('locations', this.model); - }, - - select() { - this.$el.addClass('selected'); - }, - - unselect() { - this.$el.removeClass('selected'); - }, - - onTransition(transition) { - if (transition === 'falsepositive' || transition === 'wontfix') { - this.comment({ fromTransition: true }); - } - }, - - handleClick(e) { - e.preventDefault(); - const { onClick } = this.options; - if (onClick) { - onClick(this.model.get('key')); - } - }, - - filterSimilarIssues(e) { - this.options.onFilterClick(e); - }, - - onIssueCheck(e) { - this.options.onCheck(e); - }, - - onPermalinkClick(e) { - e.stopPropagation(); - }, - - serializeData() { - const issueKey = encodeURIComponent(this.model.get('key')); - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - permalink: window.baseUrl + '/issues/search#issues=' + issueKey, - hasSecondaryLocations: this.model.get('flows').length, - hasSimilarIssuesFilter: this.options.onFilterClick != null, - hasCheckbox: this.options.onCheck != null, - checked: this.options.checked - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/models/issue.js b/server/sonar-web/src/main/js/components/issue/models/issue.js deleted file mode 100644 index 1abeee02e24..00000000000 --- a/server/sonar-web/src/main/js/components/issue/models/issue.js +++ /dev/null @@ -1,281 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Backbone from 'backbone'; - -export default Backbone.Model.extend({ - idAttribute: 'key', - - defaults() { - return { - flows: [] - }; - }, - - url() { - return window.baseUrl + '/api/issues'; - }, - - urlRoot() { - return window.baseUrl + '/api/issues'; - }, - - parse(r) { - let issue = Array.isArray(r.issues) && r.issues.length > 0 ? r.issues[0] : r.issue; - if (issue) { - issue = this._injectRelational(issue, r.components, 'component', 'key'); - issue = this._injectRelational(issue, r.components, 'project', 'key'); - issue = this._injectRelational(issue, r.components, 'subProject', 'key'); - issue = this._injectRelational(issue, r.rules, 'rule', 'key'); - issue = this._injectRelational(issue, r.users, 'assignee', 'login'); - issue = this._injectCommentsRelational(issue, r.users); - issue = this._prepareClosed(issue); - issue = this.ensureTextRange(issue); - return issue; - } else { - return r; - } - }, - - _injectRelational(issue, source, baseField, lookupField) { - const baseValue = issue[baseField]; - if (baseValue != null && Array.isArray(source) && source.length > 0) { - const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); - if (lookupValue != null) { - Object.keys(lookupValue).forEach(key => { - const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1); - issue[newKey] = lookupValue[key]; - }); - } - } - return issue; - }, - - _injectCommentsRelational(issue, users) { - if (issue.comments) { - const newComments = issue.comments.map(comment => { - let newComment = { ...comment, author: comment.login }; - delete newComment.login; - newComment = this._injectRelational(newComment, users, 'author', 'login'); - return newComment; - }); - return { ...issue, comments: newComments }; - } - return issue; - }, - - _prepareClosed(issue) { - if (issue.status === 'CLOSED') { - issue.flows = []; - delete issue.textRange; - } - return issue; - }, - - ensureTextRange(issue) { - if (issue.line && !issue.textRange) { - // FIXME 999999 - issue.textRange = { - startLine: issue.line, - endLine: issue.line, - startOffset: 0, - endOffset: 999999 - }; - } - return issue; - }, - - sync(method, model, options) { - const opts = options || {}; - opts.contentType = 'application/x-www-form-urlencoded'; - if (method === 'read') { - Object.assign(opts, { - type: 'GET', - url: this.urlRoot() + '/search', - data: { - issues: model.id, - additionalFields: '_all' - } - }); - } - if (method === 'create') { - Object.assign(opts, { - type: 'POST', - url: this.urlRoot() + '/create', - data: { - component: model.get('component'), - line: model.get('line'), - message: model.get('message'), - rule: model.get('rule'), - severity: model.get('severity') - } - }); - } - const xhr = (options.xhr = Backbone.ajax(opts)); - model.trigger('request', model, xhr, opts); - return xhr; - }, - - /** - * Reset issue attributes (delete old, replace with new) - * @param attrs - * @param options - * @returns {Object} - */ - reset(attrs, options) { - for (const key in this.attributes) { - if (this.attributes.hasOwnProperty(key) && !(key in attrs)) { - attrs[key] = void 0; - } - } - return this.set(attrs, options); - }, - - /** - * Do an action over an issue - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - * @private - */ - _action(options) { - const that = this; - const success = function(r) { - const attrs = that.parse(r); - that.reset(attrs); - if (options.success) { - options.success(that, r, options); - } - }; - const opts = { type: 'POST', ...options, success }; - const xhr = (options.xhr = Backbone.ajax(opts)); - this.trigger('request', this, xhr, opts); - return xhr; - }, - - /** - * Assign issue - * @param {String|null} assignee Assignee, can be null to unassign issue - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - assign(assignee, options) { - const opts = { - url: this.urlRoot() + '/assign', - data: { issue: this.id, assignee }, - ...options - }; - return this._action(opts); - }, - - /** - * Plan issue - * @param {String|null} plan Action Plan, can be null to unplan issue - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - plan(plan, options) { - const opts = { - url: this.urlRoot() + '/plan', - data: { issue: this.id, plan }, - ...options - }; - return this._action(opts); - }, - - /** - * Set severity of issue - * @param {String|null} severity Severity - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - setSeverity(severity, options) { - const opts = { - url: this.urlRoot() + '/set_severity', - data: { issue: this.id, severity }, - ...options - }; - return this._action(opts); - }, - - /** - * Do transition on issue - * @param {String|null} transition Transition - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - transition(transition, options) { - const that = this; - const opts = { - url: this.urlRoot() + '/do_transition', - data: { issue: this.id, transition }, - ...options - }; - return this._action(opts).done(() => { - that.trigger('transition', transition); - }); - }, - - /** - * Set type of issue - * @param {String|null} issueType Issue type - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - setType(issueType, options) { - const opts = { - url: this.urlRoot() + '/set_type', - data: { issue: this.id, type: issueType }, - ...options - }; - return this._action(opts); - }, - - /** - * Do a custom (plugin) action - * @param {String} actionKey Action Key - * @param {Object|null} options Options for jQuery ajax - * @returns {jqXHR} - */ - customAction(actionKey, options) { - const opts = { - type: 'POST', - url: this.urlRoot() + '/do_action', - data: { issue: this.id, actionKey }, - ...options - }; - const xhr = Backbone.ajax(opts); - this.trigger('request', this, xhr, opts); - return xhr; - }, - - getLinearLocations() { - const textRange = this.get('textRange'); - if (!textRange) { - return []; - } - const locations = []; - for (let line = textRange.startLine; line <= textRange.endLine; line++) { - // TODO fix 999999 - const from = line === textRange.startLine ? textRange.startOffset : 0; - const to = line === textRange.endLine ? textRange.endOffset : 999999; - locations.push({ line, from, to }); - } - return locations; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js new file mode 100644 index 00000000000..88d352e9950 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js @@ -0,0 +1,137 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import BubblePopup from '../../../components/common/BubblePopup'; +import SelectList from '../../../components/common/SelectList'; +import SelectListItem from '../../../components/common/SelectListItem'; +import SeverityHelper from '../../../components/shared/SeverityHelper'; +import StatusHelper from '../../../components/shared/StatusHelper'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import Avatar from '../../../components/ui/Avatar'; +import { translate } from '../../../helpers/l10n'; +import { fileFromPath, limitComponentName } from '../../../helpers/path'; +import type { Issue } from '../types'; + +type Props = {| + issue: Issue, + onFilter: (property: string, issue: Issue) => void, + popupPosition?: {} +|}; + +export default class SimilarIssuesPopup extends React.PureComponent { + props: Props; + + handleSelect = (property: string) => { + this.props.onFilter(property, this.props.issue); + }; + + render() { + const { issue } = this.props; + + const items = [ + 'type', + 'severity', + 'status', + 'resolution', + 'assignee', + 'rule', + ...(issue.tags || []).map(tag => `tag###${tag}`), + 'project', + // $FlowFixMe items are filtered later + issue.subProject ? 'module' : undefined, + 'file' + ].filter(item => item); + + return ( + +
    +
    {translate('issue.filter_similar_issues')}
    +
    + + + + + {translate('issue.type', issue.type)} + + + + + + + + + + + + {issue.resolution != null + ? translate('issue.resolution', issue.resolution) + : translate('unresolved')} + + + + {issue.assignee != null + ? + {translate('assigned_to')} + + {issue.assigneeName} + + : translate('unassigned')} + + + + {limitComponentName(issue.ruleName)} + + + {issue.tags != null && + issue.tags.map(tag => ( + + + {tag} + + ))} + + + + {issue.projectName} + + + {issue.subProject != null && + + + {issue.subProjectName} + } + + + + {fileFromPath(issue.componentLongName)} + + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js index 6c4f9d5977e..35d5c05b5f2 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js @@ -21,6 +21,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import ChangelogPopup from '../ChangelogPopup'; +jest.mock('moment', () => () => ({ format: () => 'March 1, 2017 9:36 AM' })); + it('should render the changelog popup correctly', () => { const element = shallow( -
    {{t 'issue.comment.delete_confirm_message'}}
    - - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs deleted file mode 100644 index d88b8a7da8d..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs +++ /dev/null @@ -1,18 +0,0 @@ -
    - -
    - -
    - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs deleted file mode 100644 index 29550cde4da..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
  • - - {{text}} - -
  • diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs deleted file mode 100644 index 64d2d0d7166..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs deleted file mode 100644 index 7ba2e7c2937..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs +++ /dev/null @@ -1,37 +0,0 @@ -
    -
    - - - - - - - - - {{#each items}} - - - - - - {{/each}} - -
    {{dt issue.creationDate}} - {{#if issue.author}} - {{t 'created_by'}} {{issue.author}} - {{else}} - {{t 'created'}} - {{/if}} -
    {{dt creationDate}} - {{#if userName}} - {{#ifShowAvatars}}{{avatarHelperNew avatar 16}}{{/ifShowAvatars}} - {{/if}} - {{userName}} - - {{#each diffs}} - {{changelog this}}
    - {{/each}} -
    - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs deleted file mode 100644 index 9184dd34b64..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs +++ /dev/null @@ -1,13 +0,0 @@ - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs deleted file mode 100644 index ea6a3a92b15..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs +++ /dev/null @@ -1,11 +0,0 @@ - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs deleted file mode 100644 index 3f42921aba2..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs +++ /dev/null @@ -1,11 +0,0 @@ - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs deleted file mode 100644 index 90df7aa6e62..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs +++ /dev/null @@ -1,17 +0,0 @@ -
  • - - - {{#if selected}} - - {{else}} - - {{/if}} - - {{#if custom}} - + {{tag}} - {{else}} - {{tag}} - {{/if}} - -
  • diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs deleted file mode 100644 index 64d2d0d7166..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs deleted file mode 100644 index ef8ae2f24af..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs +++ /dev/null @@ -1,12 +0,0 @@ - - -
    diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs deleted file mode 100644 index 72e59a58a1f..00000000000 --- a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs +++ /dev/null @@ -1,182 +0,0 @@ -
    - - - - - - - -
    -
    - {{message}}  - -
    -
    -
      -
    • - -
    • - - {{#if line}} -
    • - L{{line}} -
    • - {{/if}} - - {{#if hasSecondaryLocations}} -
    • - -
    • - {{/if}} - -
    • - -
    • - - {{#if hasSimilarIssuesFilter}} -
    • - -
    • - {{/if}} -
    -
    - - - - - - - -
    -
      -
    • - {{#inArray actions "set_severity"}} - - {{else}} - {{issueTypeIcon this.type}} {{issueType this.type}} - {{/inArray}} -
    • - -
    • - {{#inArray actions "set_severity"}} - - {{else}} - {{severityHelper severity}} - {{/inArray}} -
    • - -
    • - {{#notEmpty transitions}} - - {{else}} - {{statusHelper status resolution}} - {{/notEmpty}} -
    • - -
    • - {{#inArray actions "assign"}} - - {{else}} - {{#if assignee}} - {{#ifShowAvatars}} - {{avatarHelperNew assigneeAvatar 16}} - {{/ifShowAvatars}} - {{/if}} - {{#if assignee}}{{assigneeName}}{{else}}{{t 'unassigned'}}{{/if}} - {{/inArray}} -
    • - - {{#if debt}} -
    • - - {{tp 'issue.x_effort' debt}} - -
    • - {{/if}} - - {{#inArray actions "comment"}} -
    • - -
    • - {{/inArray}} -
    - - {{#inArray actions "assign_to_me"}} - - {{/inArray}} -
    -
      -
    • - {{#inArray actions "set_tags"}} - - {{else}} - -  {{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}} - - {{/inArray}} -
    • -
    -
    - - {{#notEmpty comments}} -
    - {{#each comments}} -
    -
    - {{#ifShowAvatars}}{{avatarHelperNew authorAvatar 16}}{{else}} - {{/ifShowAvatars}} {{authorName}} -
    -
    {{{show html htmlText}}}
    -
    ({{fromNow createdAt}})
    -
    - {{#if updatable}} - - - {{/if}} -
    -
    - {{/each}} -
    - {{/notEmpty}} - -
    - - - - - - -{{#if hasCheckbox}} -
    - -
    -{{/if}} diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js index 690c38146cb..4a07b129eeb 100644 --- a/server/sonar-web/src/main/js/components/issue/types.js +++ b/server/sonar-web/src/main/js/components/issue/types.js @@ -52,6 +52,10 @@ export type Issue = { assigneeName?: string, author?: string, comments?: Array, + 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, textRange: TextRange, transitions?: Array, diff --git a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js b/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js deleted file mode 100644 index 3360de2f416..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import PopupView from '../../common/popup'; -import Template from '../templates/DeleteComment.hbs'; - -export default PopupView.extend({ - template: Template, - - events: { - 'click button': 'handleSubmit' - }, - - handleSubmit(e) { - e.preventDefault(); - this.options.onDelete(); - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js deleted file mode 100644 index a3e81ef0dae..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { debounce, uniqBy } from 'lodash'; -import ActionOptionsView from '../../common/action-options-view'; -import Template from '../templates/issue-assign-form.hbs'; -import OptionTemplate from '../templates/issue-assign-form-option.hbs'; -import { translate } from '../../../helpers/l10n'; -import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore'; -import { areThereCustomOrganizations } from '../../../store/organizations/utils'; - -export default ActionOptionsView.extend({ - template: Template, - optionTemplate: OptionTemplate, - - events() { - return { - ...ActionOptionsView.prototype.events.apply(this, arguments), - 'click input': 'onInputClick', - 'keydown input': 'onInputKeydown', - 'keyup input': 'onInputKeyup' - }; - }, - - initialize() { - ActionOptionsView.prototype.initialize.apply(this, arguments); - this.assignees = null; - this.organizationKey = areThereCustomOrganizations() - ? this.model.get('projectOrganization') - : null; - this.debouncedSearch = debounce(this.search, 250); - }, - - getAssignee() { - return this.model.get('assignee'); - }, - - getAssigneeName() { - return this.model.get('assigneeName'); - }, - - onRender() { - const that = this; - ActionOptionsView.prototype.onRender.apply(this, arguments); - this.renderTags(); - setTimeout( - () => { - that.$('input').focus(); - }, - 100 - ); - }, - - renderTags() { - this.$('.menu').empty(); - this.getAssignees().forEach(this.renderAssignee, this); - this.bindUIElements(); - this.selectInitialOption(); - }, - - renderAssignee(assignee) { - const html = this.optionTemplate(assignee); - this.$('.menu').append(html); - }, - - selectOption(e) { - const assignee = $(e.currentTarget).data('value'); - const assigneeName = $(e.currentTarget).data('text'); - this.submit(assignee, assigneeName); - return ActionOptionsView.prototype.selectOption.apply(this, arguments); - }, - - submit(assignee) { - return this.model.assign(assignee); - }, - - onInputClick(e) { - e.stopPropagation(); - }, - - onInputKeydown(e) { - this.query = this.$('input').val(); - if (e.keyCode === 38) { - this.selectPreviousOption(); - } - if (e.keyCode === 40) { - this.selectNextOption(); - } - if (e.keyCode === 13) { - this.selectActiveOption(); - } - if (e.keyCode === 27) { - this.destroy(); - } - if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) { - return false; - } - }, - - onInputKeyup() { - let query = this.$('input').val(); - if (query !== this.query) { - if (query.length < 2) { - query = ''; - } - this.query = query; - this.debouncedSearch(query); - } - }, - - search(query) { - const that = this; - if (query.length > 1) { - const searchUrl = this.organizationKey != null - ? '/organizations/search_members' - : '/users/search'; - const queryData = { q: query }; - if (this.organizationKey != null) { - queryData.organization = this.organizationKey; - } - $.get(window.baseUrl + '/api' + searchUrl, queryData).done(data => { - that.resetAssignees(data.users); - }); - } else { - this.resetAssignees(); - } - }, - - resetAssignees(users) { - if (users) { - this.assignees = users.map(user => { - return { id: user.login, text: user.name }; - }); - } else { - this.assignees = null; - } - this.renderTags(); - }, - - getAssignees() { - if (this.assignees) { - return this.assignees; - } - const currentUser = getCurrentUserFromStore(); - const assignees = [ - { id: currentUser.login, text: currentUser.name }, - { id: '', text: translate('unassigned') } - ]; - return this.makeUnique(assignees); - }, - - makeUnique(assignees) { - return uniqBy(assignees, assignee => assignee.id); - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js b/server/sonar-web/src/main/js/components/issue/views/changelog-view.js deleted file mode 100644 index b04b56abd6a..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import PopupView from '../../common/popup'; -import Template from '../templates/issue-changelog.hbs'; - -export default PopupView.extend({ - template: Template, - - collectionEvents: { - sync: 'render' - }, - - serializeData() { - return { - ...PopupView.prototype.serializeData.apply(this, arguments), - issue: this.options.issue.toJSON() - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js b/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js deleted file mode 100644 index 52d68bcd7c0..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import PopupView from '../../common/popup'; -import Template from '../templates/comment-form.hbs'; - -export default PopupView.extend({ - className: 'bubble-popup issue-comment-bubble-popup', - template: Template, - - ui: { - textarea: '.issue-comment-form-text textarea', - cancelButton: '.js-issue-comment-cancel', - submitButton: '.js-issue-comment-submit' - }, - - events: { - click: 'onClick', - 'keydown @ui.textarea': 'onKeydown', - 'keyup @ui.textarea': 'toggleSubmit', - 'click @ui.cancelButton': 'cancel', - 'click @ui.submitButton': 'submit' - }, - - onRender() { - const that = this; - PopupView.prototype.onRender.apply(this, arguments); - setTimeout( - () => { - that.ui.textarea.focus(); - }, - 100 - ); - }, - - toggleSubmit() { - this.ui.submitButton.prop('disabled', this.ui.textarea.val().length === 0); - }, - - onClick(e) { - e.stopPropagation(); - }, - - onKeydown(e) { - if (e.keyCode === 27) { - this.destroy(); - } - if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { - this.submit(); - } - }, - - cancel() { - this.options.detailView.updateAfterAction(); - }, - - disableForm() { - this.$(':input').prop('disabled', true); - }, - - enableForm() { - this.$(':input').prop('disabled', false); - }, - - submit() { - const text = this.ui.textarea.val(); - - if (!text.length) { - return; - } - - const update = this.model && this.model.has('key'); - const method = update ? 'edit_comment' : 'add_comment'; - const url = window.baseUrl + '/api/issues/' + method; - const data = { text }; - if (update) { - data.key = this.model.get('key'); - } else { - data.issue = this.options.issue.id; - } - this.disableForm(); - this.options.detailView.disableControls(); - $.post(url, data).done(r => this.options.detailView.updateAfterAction(r)).fail(() => { - this.enableForm(); - this.options.detailView.enableControls(); - }); - }, - - serializeData() { - const options = { fromTransition: false, ...this.options.additionalOptions }; - return { - ...PopupView.prototype.serializeData.apply(this, arguments), - options - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js b/server/sonar-web/src/main/js/components/issue/views/issue-popup.js deleted file mode 100644 index 96488cd1e45..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import PopupView from '../../common/popup'; - -export default PopupView.extend({ - className: 'bubble-popup issue-bubble-popup', - - template() { - return '
    '; - }, - - events() { - return { - 'click .js-issue-form-cancel': 'destroy' - }; - }, - - onRender() { - PopupView.prototype.onRender.apply(this, arguments); - this.options.view.$el.appendTo(this.$el); - this.options.view.render(); - }, - - onDestroy() { - this.options.view.destroy(); - }, - - attachCloseEvents() {} -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js deleted file mode 100644 index b30c689e1e9..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import ActionOptionsView from '../../common/action-options-view'; -import Template from '../templates/issue-set-severity-form.hbs'; - -export default ActionOptionsView.extend({ - template: Template, - - getTransition() { - return this.model.get('severity'); - }, - - selectInitialOption() { - return this.makeActive(this.getOptions().filter(`[data-value="${this.getTransition()}"]`)); - }, - - selectOption(e) { - const severity = $(e.currentTarget).data('value'); - this.submit(severity); - return ActionOptionsView.prototype.selectOption.apply(this, arguments); - }, - - submit(severity) { - return this.model.setSeverity(severity); - }, - - serializeData() { - return { - ...ActionOptionsView.prototype.serializeData.apply(this, arguments), - items: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'] - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js deleted file mode 100644 index 719d679e762..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import ActionOptionsView from '../../common/action-options-view'; -import Template from '../templates/issue-set-type-form.hbs'; - -export default ActionOptionsView.extend({ - template: Template, - - getType() { - return this.model.get('type'); - }, - - selectInitialOption() { - return this.makeActive(this.getOptions().filter(`[data-value="${this.getType()}"]`)); - }, - - selectOption(e) { - const issueType = $(e.currentTarget).data('value'); - this.submit(issueType); - return ActionOptionsView.prototype.selectOption.apply(this, arguments); - }, - - submit(issueType) { - return this.model.setType(issueType); - }, - - serializeData() { - return { - ...ActionOptionsView.prototype.serializeData.apply(this, arguments), - items: ['BUG', 'VULNERABILITY', 'CODE_SMELL'] - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js b/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js deleted file mode 100644 index 87b1287bee9..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js +++ /dev/null @@ -1,196 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { debounce, difference, without } from 'lodash'; -import ActionOptionsView from '../../common/action-options-view'; -import Template from '../templates/issue-tags-form.hbs'; -import OptionTemplate from '../templates/issue-tags-form-option.hbs'; - -export default ActionOptionsView.extend({ - template: Template, - optionTemplate: OptionTemplate, - - modelEvents: { - 'change:tags': 'renderTags' - }, - - events() { - return { - ...ActionOptionsView.prototype.events.apply(this, arguments), - 'click input': 'onInputClick', - 'keydown input': 'onInputKeydown', - 'keyup input': 'onInputKeyup' - }; - }, - - initialize() { - ActionOptionsView.prototype.initialize.apply(this, arguments); - this.query = ''; - this.tags = []; - this.selected = 0; - this.debouncedSearch = debounce(this.search, 250); - this.requestTags(); - }, - - requestTags(query) { - const that = this; - return $.get(window.baseUrl + '/api/issues/tags', { ps: 10, q: query }).done(data => { - that.tags = data.tags; - that.renderTags(); - }); - }, - - onRender() { - const that = this; - ActionOptionsView.prototype.onRender.apply(this, arguments); - this.renderTags(); - setTimeout( - () => { - that.$('input').focus(); - }, - 100 - ); - }, - - selectInitialOption() { - this.selected = Math.max(Math.min(this.selected, this.getOptions().length - 1), 0); - this.makeActive(this.getOptions().eq(this.selected)); - }, - - filterTags(tags) { - return tags.filter(tag => tag.indexOf(this.query) !== -1); - }, - - renderTags() { - this.$('.menu').empty(); - this.filterTags(this.getTags()).forEach(this.renderSelectedTag, this); - this.filterTags(difference(this.tags, this.getTags())).forEach(this.renderTag, this); - if ( - this.query.length > 0 && - this.tags.indexOf(this.query) === -1 && - this.getTags().indexOf(this.query) === -1 - ) { - this.renderCustomTag(this.query); - } - this.selectInitialOption(); - }, - - renderSelectedTag(tag) { - const html = this.optionTemplate({ - tag, - selected: true, - custom: false - }); - return this.$('.menu').append(html); - }, - - renderTag(tag) { - const html = this.optionTemplate({ - tag, - selected: false, - custom: false - }); - return this.$('.menu').append(html); - }, - - renderCustomTag(tag) { - const html = this.optionTemplate({ - tag, - selected: false, - custom: true - }); - return this.$('.menu').append(html); - }, - - selectOption(e) { - e.preventDefault(); - e.stopPropagation(); - let tags = this.getTags().slice(); - const tag = $(e.currentTarget).data('value'); - if ($(e.currentTarget).data('selected') != null) { - tags = without(tags, tag); - } else { - tags.push(tag); - } - this.selected = this.getOptions().index($(e.currentTarget)); - return this.submit(tags); - }, - - submit(tags) { - const that = this; - const _tags = this.getTags(); - this.model.set({ tags }); - return $.ajax({ - type: 'POST', - url: window.baseUrl + '/api/issues/set_tags', - data: { - key: this.model.id, - tags: tags.join() - } - }).fail(() => that.model.set({ tags: _tags })); - }, - - onInputClick(e) { - e.stopPropagation(); - }, - - onInputKeydown(e) { - this.query = this.$('input').val(); - if (e.keyCode === 38) { - this.selectPreviousOption(); - } - if (e.keyCode === 40) { - this.selectNextOption(); - } - if (e.keyCode === 13) { - this.selectActiveOption(); - } - if (e.keyCode === 27) { - this.destroy(); - } - if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) { - return false; - } - }, - - onInputKeyup() { - const query = this.$('input').val(); - if (query !== this.query) { - this.query = query; - this.debouncedSearch(query); - } - }, - - search(query) { - this.query = query; - return this.requestTags(query); - }, - - resetAssignees(users) { - this.assignees = users.map(user => { - return { id: user.login, text: user.name }; - }); - this.renderTags(); - }, - - getTags() { - return this.model.get('tags') || []; - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js b/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js deleted file mode 100644 index 0a44b5a4b22..00000000000 --- a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import ActionOptionsView from '../../common/action-options-view'; -import Template from '../templates/issue-transitions-form.hbs'; - -export default ActionOptionsView.extend({ - template: Template, - - selectInitialOption() { - this.makeActive(this.getOptions().first()); - }, - - selectOption(e) { - const transition = $(e.currentTarget).data('value'); - this.submit(transition); - return ActionOptionsView.prototype.selectOption.apply(this, arguments); - }, - - submit(transition) { - return this.model.transition(transition); - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js b/server/sonar-web/src/main/js/components/layout/Page.js similarity index 67% rename from server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js rename to server/sonar-web/src/main/js/components/layout/Page.js index 8d15af06288..a8adef56e19 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js +++ b/server/sonar-web/src/main/js/components/layout/Page.js @@ -18,12 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import { connect } from 'react-redux'; -import LineIssuesIndicator from './LineIssuesIndicator'; -import { getIssueByKey } from '../../../store/rootReducer'; +import React from 'react'; +import { css } from 'glamor'; -const mapStateToProps = (state, ownProps: { issueKeys: Array }) => ({ - issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey)) +type Props = { + className?: string, + children?: React.Element<*> +}; + +const styles = css({ + display: 'flex', + alignItems: 'stretch', + width: '100%', + flexGrow: 1 }); -export default connect(mapStateToProps)(LineIssuesIndicator); +const Page = ({ className, children, ...other }: Props) => ( +
    + {children} +
    +); + +export default Page; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js b/server/sonar-web/src/main/js/components/layout/PageFilters.js similarity index 76% rename from server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js rename to server/sonar-web/src/main/js/components/layout/PageFilters.js index cd9aaf55d66..f969366de69 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/componentIssuesPermalink.js +++ b/server/sonar-web/src/main/js/components/layout/PageFilters.js @@ -17,6 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -module.exports = function(componentKey) { - return window.baseUrl + '/component_issues/index?id=' + encodeURIComponent(componentKey); +// @flow +import React from 'react'; +import { css } from 'glamor'; + +type Props = { + children?: React.Element<*> }; + +const PageSide = (props: Props) => ( +
    + {props.children} +
    +); + +export default PageSide; diff --git a/server/sonar-web/src/main/js/components/layout/PageMain.js b/server/sonar-web/src/main/js/components/layout/PageMain.js new file mode 100644 index 00000000000..6195a1f651a --- /dev/null +++ b/server/sonar-web/src/main/js/components/layout/PageMain.js @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css } from 'glamor'; + +type Props = { + children?: React.Element<*> +}; + +const PageMain = (props: Props) => ( +
    + {props.children} +
    +); + +export default PageMain; diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js b/server/sonar-web/src/main/js/components/layout/PageMainInner.js similarity index 75% rename from server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js rename to server/sonar-web/src/main/js/components/layout/PageMainInner.js index eea72dc3e28..41beed6518f 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js +++ b/server/sonar-web/src/main/js/components/layout/PageMainInner.js @@ -17,13 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import Marionette from 'backbone.marionette'; -import { translate } from '../../helpers/l10n'; +// @flow +import React from 'react'; +import { css } from 'glamor'; -export default Marionette.ItemView.extend({ - className: 'search-navigator-no-results', +type Props = { + children?: React.Element<*> +}; - template() { - return translate('issue_filter.no_issues'); - } -}); +const PageMainInner = (props: Props) => ( +
    + {props.children} +
    +); + +export default PageMainInner; diff --git a/server/sonar-web/src/main/js/components/layout/PageSide.js b/server/sonar-web/src/main/js/components/layout/PageSide.js new file mode 100644 index 00000000000..0488fbfceb9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/layout/PageSide.js @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css, media } from 'glamor'; + +type Props = { + children?: React.Element<*>, + top?: number +}; + +const width = css( + { + width: 'calc(50vw - 360px)' + }, + media('(max-width: 1320px)', { width: 300 }) +); + +const sideStyles = css(width, { + flexGrow: 0, + flexShrink: 0, + borderRight: '1px solid #e6e6e6', + backgroundColor: '#f3f3f3' +}); + +const sideStickyStyles = css(width, { + position: 'fixed', + zIndex: 40, + top: 0, + bottom: 0, + left: 0, + overflowY: 'auto', + overflowX: 'hidden', + backgroundColor: '#f3f3f3' +}); + +const sideInnerStyles = css( + { + width: 300, + marginLeft: 'calc(50vw - 660px)', + backgroundColor: '#f3f3f3' + }, + media('(max-width: 1320px)', { marginLeft: 0 }) +); + +const PageSide = (props: Props) => ( +
    +
    +
    + {props.children} +
    +
    +
    +); + +export default PageSide; diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js index d47b6e82ba4..b7b35b539c5 100644 --- a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js +++ b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js @@ -20,6 +20,7 @@ import $ from 'jquery'; import { throttle } from 'lodash'; import Marionette from 'backbone.marionette'; +import key from 'keymaster'; const BOTTOM_OFFSET = 60; diff --git a/server/sonar-web/src/main/js/components/shared/Organization.js b/server/sonar-web/src/main/js/components/shared/Organization.js index 807abd3d11e..ac723440c9c 100644 --- a/server/sonar-web/src/main/js/components/shared/Organization.js +++ b/server/sonar-web/src/main/js/components/shared/Organization.js @@ -29,6 +29,7 @@ type OwnProps = { type Props = { link?: boolean, + linkClassName?: string, organizationKey: string, organization: { key: string, name: string } | null, shouldBeDisplayed: boolean @@ -51,7 +52,9 @@ class Organization extends React.Component { return ( {this.props.link - ? {organization.name} + ? + {organization.name} + : organization.name} diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js similarity index 68% rename from server/sonar-web/src/main/js/apps/projects/components/NoProjects.js rename to server/sonar-web/src/main/js/components/shared/QualifierIcon.js index c2194ad5409..82ed9f7e5e1 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js +++ b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js @@ -18,15 +18,26 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { translate } from '../../../helpers/l10n'; +import classNames from 'classnames'; + +type Props = { + className?: string, + qualifier: ?string +}; + +export default class QualifierIcon extends React.PureComponent { + props: Props; -export default class NoProjects extends React.Component { render() { - return ( -
    -

    {translate('projects.no_projects.1')}

    -

    {translate('projects.no_projects.2')}

    -
    + if (!this.props.qualifier) { + return null; + } + + const className = classNames( + 'icon-qualifier-' + this.props.qualifier.toLowerCase(), + this.props.className ); + + return ; } } diff --git a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js similarity index 60% rename from server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js rename to server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js index dd54e82c8d2..857ccbaf450 100644 --- a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js +++ b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js @@ -17,24 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import $ from 'jquery'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-mode-facet.hbs'; +import React from 'react'; +import { shallow } from 'enzyme'; +import QualifierIcon from '../QualifierIcon'; -export default BaseFacet.extend({ - template: Template, +it('should render icon', () => { + expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); +}); - toggleFacet(e) { - const isCount = $(e.currentTarget).is('[data-value="count"]'); - return this.options.app.state.updateFilter({ - facetMode: isCount ? 'count' : 'effort' - }); - }, +it('should not render icon', () => { + expect(shallow()).toMatchSnapshot(); +}); - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - mode: this.options.app.state.getFacetMode() - }; - } +it('should render with custom class', () => { + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap new file mode 100644 index 00000000000..58ac761a183 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap @@ -0,0 +1,16 @@ +exports[`test should not render icon 1`] = `null`; + +exports[`test should render icon 1`] = ` + +`; + +exports[`test should render icon 2`] = ` + +`; + +exports[`test should render with custom class 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js b/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js new file mode 100644 index 00000000000..04654471d59 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/issues-test.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import { parseIssueFromResponse } from '../issues'; + +it('should populate comments data', () => { + const users = [ + { + active: true, + avatar: 'c1244e6857f7be3dc4549d9e9d51c631', + login: 'admin', + name: 'Admin Admin' + } + ]; + const issue = { + comments: [ + { + createdAt: '2017-04-11T10:38:09+0200', + htmlText: 'comment!', + key: 'AVtcKbZkQmGLa7yW8J71', + login: 'admin', + markdown: 'comment!', + updatable: true + } + ] + }; + expect(parseIssueFromResponse(issue, undefined, users, undefined)).toEqual({ + comments: [ + { + author: 'admin', + authorActive: true, + authorAvatar: 'c1244e6857f7be3dc4549d9e9d51c631', + authorLogin: 'admin', + authorName: 'Admin Admin', + createdAt: '2017-04-11T10:38:09+0200', + htmlText: 'comment!', + key: 'AVtcKbZkQmGLa7yW8J71', + login: undefined, + markdown: 'comment!', + updatable: true + } + ] + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js index c6e54c559d5..c2047a572a4 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js @@ -55,32 +55,17 @@ describe('#getComponentUrl', () => { describe('#getComponentIssuesUrl', () => { it('should work without parameters', () => { - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toBe( - '/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#' - ); - }); - - it('should encode component key', () => { - expect(getComponentIssuesUrl(COMPLEX_COMPONENT_KEY, {})).toBe( - '/component_issues?id=' + COMPLEX_COMPONENT_KEY_ENCODED + '#' - ); + expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({ + pathname: '/project/issues', + query: { id: SIMPLE_COMPONENT_KEY } + }); }); it('should work with parameters', () => { - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toBe( - '/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#resolved=false' - ); - }); - - it('should encode parameters', () => { - expect( - getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { componentUuids: COMPLEX_COMPONENT_KEY }) - ).toBe( - '/component_issues?id=' + - SIMPLE_COMPONENT_KEY + - '#componentUuids=' + - COMPLEX_COMPONENT_KEY_ENCODED - ); + expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual({ + pathname: '/project/issues', + query: { id: SIMPLE_COMPONENT_KEY, resolved: 'false' } + }); }); }); diff --git a/server/sonar-web/src/main/js/helpers/issues.js b/server/sonar-web/src/main/js/helpers/issues.js index e8ce3a697ec..013831fc1e6 100644 --- a/server/sonar-web/src/main/js/helpers/issues.js +++ b/server/sonar-web/src/main/js/helpers/issues.js @@ -20,6 +20,7 @@ // @flow import { sortBy } from 'lodash'; import { SEVERITIES } from './constants'; +import type { Issue } from '../components/issue/types'; type TextRange = { startLine: number, @@ -83,12 +84,13 @@ const injectCommentsRelational = (issue: RawIssue, users?: Array) => { if (!issue.comments) { return {}; } - const comments = issue.comments.map(comment => ({ - ...comment, - author: comment.login, - login: undefined, - ...injectRelational(comment, users, 'author', 'login') - })); + const comments = issue.comments.map(comment => { + const commentWithAuthor = { ...comment, author: comment.login, login: undefined }; + return { + ...commentWithAuthor, + ...injectRelational(commentWithAuthor, users, 'author', 'login') + }; + }); return { comments }; }; @@ -110,11 +112,11 @@ const ensureTextRange = (issue: RawIssue) => { }; export const parseIssueFromResponse = ( - issue: RawIssue, + issue: Object, components?: Array<*>, users?: Array<*>, rules?: Array<*> -) => { +): Issue => { return { ...issue, ...injectRelational(issue, components, 'component', 'key'), diff --git a/server/sonar-web/src/main/js/helpers/path.js b/server/sonar-web/src/main/js/helpers/path.js index 63153bf6dc8..b65c049f3e4 100644 --- a/server/sonar-web/src/main/js/helpers/path.js +++ b/server/sonar-web/src/main/js/helpers/path.js @@ -102,3 +102,12 @@ export function splitPath(path) { return null; } } + +export function limitComponentName(str) { + if (typeof str === 'string') { + const LIMIT = 30; + return str.length > LIMIT ? str.substr(0, LIMIT) + '...' : str; + } else { + return ''; + } +} diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js index 602eb3c5786..96b718cc64b 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.js +++ b/server/sonar-web/src/main/js/helpers/testUtils.js @@ -24,7 +24,7 @@ export const mockEvent = { stopPropagation() {} }; -export const click = element => element.simulate('click', mockEvent); +export const click = (element, event = {}) => element.simulate('click', { ...mockEvent, ...event }); export const submit = element => element.simulate('submit', { @@ -37,4 +37,7 @@ export const change = (element, value) => currentTarget: { value } }); -export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode }); +export const keydown = keyCode => { + const event = new KeyboardEvent('keydown', { keyCode }); + document.dispatchEvent(event); +}; diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 34b3b06d89d..f8e127b89a0 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -37,40 +37,23 @@ export function getProjectUrl(key) { /** * Generate URL for a global issues page - * @param {object} query - * @returns {string} */ export function getIssuesUrl(query) { - const serializedQuery = Object.keys(query) - .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`) - .join('|'); - - // return a string (not { pathname }) to help react-router's Link handle this properly - return '/issues#' + serializedQuery; + return { pathname: '/issues', query }; } /** * Generate URL for a component's issues page - * @param {string} componentKey - * @param {object} query - * @returns {string} */ export function getComponentIssuesUrl(componentKey, query) { - const serializedQuery = Object.keys(query) - .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`) - .join('|'); - - // return a string (not { pathname }) to help react-router's Link handle this properly - return '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery; + return { pathname: '/project/issues', query: { ...query, id: componentKey } }; } /** * Generate URL for a single issue - * @param {string} issueKey - * @returns {string} */ -export function getSingleIssueUrl(issueKey) { - return window.baseUrl + '/issues/search#issues=' + issueKey; +export function getSingleIssueUrl(issues) { + return { pathname: '/issues', query: { issues } }; } /** diff --git a/server/sonar-web/src/main/js/libs/third-party/keymaster.js b/server/sonar-web/src/main/js/libs/third-party/keymaster.js deleted file mode 100644 index 8ba7aad8487..00000000000 --- a/server/sonar-web/src/main/js/libs/third-party/keymaster.js +++ /dev/null @@ -1,314 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// keymaster.js -// version: 1.6.2 -// (c) 2011-2013 Thomas Fuchs -// keymaster.js may be freely distributed under the MIT license. - -;(function(global){ - var k, - _handlers = {}, - _mods = { 16: false, 18: false, 17: false, 91: false }, - _scope = 'all', - // modifier keys - _MODIFIERS = { - '⇧': 16, shift: 16, - '⌥': 18, alt: 18, option: 18, - '⌃': 17, ctrl: 17, control: 17, - '⌘': 91, command: 91 - }, - // special keys - _MAP = { - backspace: 8, tab: 9, clear: 12, - enter: 13, 'return': 13, - esc: 27, escape: 27, space: 32, - left: 37, up: 38, - right: 39, down: 40, - del: 46, 'delete': 46, - home: 36, end: 35, - pageup: 33, pagedown: 34, - ',': 188, '.': 190, '/': 191, - '`': 192, '-': 189, '=': 187, - ';': 186, '\'': 222, - '[': 219, ']': 221, '\\': 220 - }, - code = function(x){ - return _MAP[x] || x.toUpperCase().charCodeAt(0); - }, - _downKeys = []; - - for(k=1;k<20;k++) _MAP['f'+k] = 111+k; - - // IE doesn't support Array#indexOf, so have a simple replacement - function index(array, item){ - var i = array.length; - while(i--) if(array[i]===item) return i; - return -1; - } - - // for comparing mods before unassignment - function compareArray(a1, a2) { - if (a1.length != a2.length) return false; - for (var i = 0; i < a1.length; i++) { - if (a1[i] !== a2[i]) return false; - } - return true; - } - - var modifierMap = { - 16:'shiftKey', - 18:'altKey', - 17:'ctrlKey', - 91:'metaKey' - }; - function updateModifierKey(event) { - for(k in _mods) _mods[k] = event[modifierMap[k]]; - }; - - // handle keydown event - function dispatch(event) { - var key, handler, k, i, modifiersMatch, scope; - key = event.keyCode; - - if (index(_downKeys, key) == -1) { - _downKeys.push(key); - } - - // if a modifier key, set the key. 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. to false - for(k in _MODIFIERS) assignKey[k] = false; - - // set current scope (default 'all') - function setScope(scope){ _scope = scope || 'all' }; - function getScope(){ return _scope || 'all' }; - - // delete all handlers for a given scope - function deleteScope(scope){ - var key, handlers, i; - - for (key in _handlers) { - handlers = _handlers[key]; - for (i = 0; i < handlers.length; ) { - if (handlers[i].scope === scope) handlers.splice(i, 1); - else i++; - } - } - }; - - // abstract key logic for assign and unassign - function getKeys(key) { - var keys; - key = key.replace(/\s/g, ''); - keys = key.split(','); - if ((keys[keys.length - 1]) == '') { - keys[keys.length - 2] += ','; - } - return keys; - } - - // abstract mods logic for assign and unassign - function getMods(key) { - var mods = key.slice(0, key.length - 1); - for (var mi = 0; mi < mods.length; mi++) - mods[mi] = _MODIFIERS[mods[mi]]; - return mods; - } - - // cross-browser events - function addEvent(object, event, method) { - if (object.addEventListener) - object.addEventListener(event, method, false); - else if(object.attachEvent) - object.attachEvent('on'+event, function(){ method(window.event) }); - }; - - // set the handlers globally on document - addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48 - addEvent(document, 'keyup', clearModifier); - - // reset modifiers to false whenever the window is (re)focused. - addEvent(window, 'focus', resetModifiers); - - // store previously defined key - var previousKey = global.key; - - // restore previously defined key and return reference to our key object - function noConflict() { - var k = global.key; - global.key = previousKey; - return k; - } - - // set window.key and window.key.set/get/deleteScope, and the default filter - global.key = assignKey; - global.key.setScope = setScope; - global.key.getScope = getScope; - global.key.deleteScope = deleteScope; - global.key.filter = filter; - global.key.isPressed = isPressed; - global.key.getPressedKeyCodes = getPressedKeyCodes; - global.key.noConflict = noConflict; - global.key.unbind = unbindKey; - -})(window); diff --git a/server/sonar-web/src/main/js/store/issues/duck.js b/server/sonar-web/src/main/js/store/issues/duck.js deleted file mode 100644 index b559d8103c7..00000000000 --- a/server/sonar-web/src/main/js/store/issues/duck.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import { keyBy } from 'lodash'; - -type Issue = { key: string }; - -type ReceiveIssuesAction = { - type: 'RECEIVE_ISSUES', - issues: Array -}; - -type Action = ReceiveIssuesAction; - -type State = { [key: string]: Issue }; - -export const receiveIssues = (issues: Array): ReceiveIssuesAction => ({ - type: 'RECEIVE_ISSUES', - issues -}); - -const reducer = (state: State = {}, action: Action) => { - switch (action.type) { - case 'RECEIVE_ISSUES': - return { ...state, ...keyBy(action.issues, 'key') }; - default: - return state; - } -}; - -export default reducer; - -export const getIssueByKey = (state: State, key: string): ?Issue => state[key]; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 95db709a545..5404ef78f6a 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -22,7 +22,6 @@ import appState from './appState/duck'; import components, * as fromComponents from './components/reducer'; import users, * as fromUsers from './users/reducer'; import favorites, * as fromFavorites from './favorites/duck'; -import issues, * as fromIssues from './issues/duck'; import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; import notifications, * as fromNotifications from './notifications/duck'; @@ -42,7 +41,6 @@ export default combineReducers({ components, globalMessages, favorites, - issues, languages, measures, notifications, @@ -85,8 +83,6 @@ export const getUsers = state => fromUsers.getUsers(state.users); export const isFavorite = (state, componentKey) => fromFavorites.isFavorite(state.favorites, componentKey); -export const getIssueByKey = (state, key) => fromIssues.getIssueByKey(state.issues, key); - export const getComponentMeasure = (state, componentKey, metricKey) => fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey); diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less index 658462e620c..ba09fadce81 100644 --- a/server/sonar-web/src/main/less/components/issues.less +++ b/server/sonar-web/src/main/less/components/issues.less @@ -43,7 +43,7 @@ .issue-list, .issue { - max-width: 920px; + max-width: 980px; } .issue.selected { @@ -91,6 +91,8 @@ } .issue-rule { + vertical-align: top; + margin-top: 2px; padding: 0 3px; background: fade(@blue, 30%); opacity: 0.5; @@ -346,7 +348,7 @@ input.issue-action-options-search { top: 0; bottom: 0; left: 0; - cursor: pointer; + border: none; &:hover { background-color: rgba(0, 0, 0, 0.05); diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less index 9e24c26252f..cd284d7a546 100644 --- a/server/sonar-web/src/main/less/components/page.less +++ b/server/sonar-web/src/main/less/components/page.less @@ -153,10 +153,11 @@ } .page-sidebar-sticky { + width: 320px !important; padding-right: 0; .page-limited & { - margin: -20px 0; + margin: -20px 0 -20px -20px; padding-right: 0 !important; .page-sidebar-sticky-inner { @@ -166,16 +167,27 @@ .page-sidebar-sticky-inner { position: fixed; + z-index: 10; top: 30px; bottom: 0; + left: 0; overflow: auto; - width: 290px; + width: ~"calc(50vw - 640px + 280px + 3px)"; border-right: 1px solid #e6e6e6; box-sizing: border-box; background: #f3f3f3; + @media (max-width: 1335px) { + & { width: 310px; } + } + .search-navigator-facets-list { width: 260px; + margin-left: ~"calc(50vw - 640px + 290px - 260px - 37px)"; + + @media (max-width: 1335px) { + & { margin-left: 20px; } + } } } } diff --git a/server/sonar-web/src/main/less/components/react-select.less b/server/sonar-web/src/main/less/components/react-select.less index 0f496e61f58..a29d4113e52 100644 --- a/server/sonar-web/src/main/less/components/react-select.less +++ b/server/sonar-web/src/main/less/components/react-select.less @@ -101,6 +101,21 @@ white-space: nowrap; } +.Select-value svg, +.Select-value [class^="icon-"] { + padding-top: 4px; +} + +.Select-value img { + padding-top: 3px; +} + +.Select-option svg, +.Select-option img, +.Select-option [class^="icon-"] { + padding-top: 2px; +} + .has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label, .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label { color: @baseFontColor; @@ -311,26 +326,15 @@ padding: 8px 10px; } -.Select--multi .Select-input { - vertical-align: middle; - margin-left: 10px; - padding: 0; -} - -.Select--multi.has-value .Select-input { - margin-left: 5px; -} - .Select--multi .Select-value { background-color: rgba(0, 126, 255, 0.08); border-radius: 2px; border: 1px solid rgba(0, 126, 255, 0.24); - color: #007eff; + color: @baseFontColor; display: inline-block; - font-size: 0.9em; - line-height: 1.4; - margin-left: 5px; - margin-top: 5px; + font-size: 12px; + line-height: 14px; + margin: 1px 4px 1px 1px; vertical-align: top; } @@ -400,6 +404,10 @@ background-color: #fcfcfc; } +.Select-aria-only { + display: none; +} + @keyframes Select-animation-spin { to { transform: rotate(1turn); diff --git a/server/sonar-web/src/main/less/components/search-navigator.less b/server/sonar-web/src/main/less/components/search-navigator.less index 08d9a90c69a..8b0203bfd2a 100644 --- a/server/sonar-web/src/main/less/components/search-navigator.less +++ b/server/sonar-web/src/main/less/components/search-navigator.less @@ -122,23 +122,30 @@ white-space: normal; overflow: hidden; font-size: 0; - cursor: pointer; transition: none; - &:hover { - border: 1px solid @blue; - padding: 3px 5px; + a& { + cursor: pointer; - .facet-stat { - top: -1px; - right: -1px; + .facet-name { + color: @baseFontColor; + } + + &:hover, &:focus { + border: 1px solid @blue; + padding: 3px 5px; + + .facet-stat { + top: -1px; + right: -1px; + } } } .facet-name { line-height: 16px; background-color: @barBackgroundColor; - color: @baseFontColor; + color: @secondFontColor; font-size: @smallFontSize; white-space: nowrap; } @@ -425,6 +432,7 @@ .search-navigator-date-facet-selection { .clearfix; position: relative; + padding: 0 10px; font-size: @smallFontSize; } diff --git a/server/sonar-web/src/main/less/pages/issues.less b/server/sonar-web/src/main/less/pages/issues.less index 3c25afc7d90..c6b9336153e 100644 --- a/server/sonar-web/src/main/less/pages/issues.less +++ b/server/sonar-web/src/main/less/pages/issues.less @@ -39,10 +39,29 @@ position: absolute; visibility: hidden; } + + .search-navigator-facet-header, + .search-navigator-facet-list { + padding-left: 0; + padding-right: 0; + } + + .search-navigator-facet-header { + padding-top: 8px; + padding-bottom: 8px; + } + + .search-navigator-facet-box:not(.hidden) + .search-navigator-facet-box { + border-top: none; + } + + .search-navigator-facet-footer { + padding: 0 0 10px 0; + } } .issues-workspace-list-component { - padding: 0 10px; + padding: 10px 10px 6px; } .issues-workspace-list-item + .issues-workspace-list-item { @@ -53,8 +72,12 @@ margin-top: 10px; } -.issues-workspace-list-item + .issues-workspace-list-component { - margin-top: 25px; +.issues-workspace-list-item:first-child .issues-workspace-list-component { + padding-top: 0; +} + +.issues-workspace-list-component + .issues-workspace-list-item { + margin-top: 0; } .issues-workspace-component-viewer { diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index e102c368f61..382177d6933 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -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" diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 9804977b607..a4456f9f574 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 diff --git a/tests/perf/src/test/java/org/sonarsource/sonarqube/perf/server/WebTest.java b/tests/perf/src/test/java/org/sonarsource/sonarqube/perf/server/WebTest.java index 357a292db1b..658e2c57ab7 100644 --- a/tests/perf/src/test/java/org/sonarsource/sonarqube/perf/server/WebTest.java +++ b/tests/perf/src/test/java/org/sonarsource/sonarqube/perf/server/WebTest.java @@ -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); } diff --git a/tests/upgrade/src/test/java/org/sonarsource/sonarqube/upgrade/UpgradeTest.java b/tests/upgrade/src/test/java/org/sonarsource/sonarqube/upgrade/UpgradeTest.java index 96c0d296960..4f08a98eeff 100644 --- a/tests/upgrade/src/test/java/org/sonarsource/sonarqube/upgrade/UpgradeTest.java +++ b/tests/upgrade/src/test/java/org/sonarsource/sonarqube/upgrade/UpgradeTest.java @@ -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"); } -- 2.39.5