diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
131 files changed, 5571 insertions, 3809 deletions
diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js index 3ae7d82992d..fd231aa512b 100644 --- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js +++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js @@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'BUG' })} + to={getIssuesUrl({ resolved: 'false', types: 'BUG' })} className="about-page-issue-type-link"> {formatMeasure(bugs, 'SHORT_INT')} </Link> @@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })} + to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })} className="about-page-issue-type-link"> {formatMeasure(vulnerabilities, 'SHORT_INT')} </Link> @@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })} + to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })} className="about-page-issue-type-link"> {formatMeasure(codeSmells, 'SHORT_INT')} </Link> diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js index 3b974ffd770..11db35cac3b 100644 --- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js +++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js @@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'BUG' })} + to={getIssuesUrl({ resolved: 'false', types: 'BUG' })} className="about-page-issue-type-link"> {formatMeasure(bugs, 'SHORT_INT')} </Link> @@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })} + to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })} className="about-page-issue-type-link"> {formatMeasure(vulnerabilities, 'SHORT_INT')} </Link> @@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { <tr> <td className="about-page-issue-type-number"> <Link - to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })} + to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })} className="about-page-issue-type-link"> {formatMeasure(codeSmells, 'SHORT_INT')} </Link> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js index 6c7d13c15b7..2a9fb4157ae 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js @@ -21,7 +21,7 @@ import React from 'react'; import { Link } from 'react-router'; import TaskType from './TaskType'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; import Organization from '../../../components/shared/Organization'; import { Task } from '../types'; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js index 9ad27ba80ec..76ea80e652f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js @@ -20,7 +20,7 @@ import React from 'react'; import { Link } from 'react-router'; import Truncated from './Truncated'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; function getTooltip(component) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/controller.js b/server/sonar-web/src/main/js/apps/coding-rules/controller.js index 01943b685bf..dd15fb39b92 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/controller.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/controller.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; +import key from 'keymaster'; import Controller from '../../components/navigator/controller'; import Rule from './models/rule'; import RuleDetailsView from './rule-details-view'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js index 57e689c5178..437505c5eaf 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/init.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/init.js @@ -22,6 +22,7 @@ import $ from 'jquery'; import { sortBy } from 'lodash'; import Backbone from 'backbone'; import Marionette from 'backbone.marionette'; +import key from 'keymaster'; import State from './models/state'; import Layout from './layout'; import Rules from './models/rules'; @@ -105,7 +106,7 @@ App.on('start', function(options: { }); this.layout.filtersRegion.show(this.filtersView); - window.key.setScope('list'); + key.setScope('list'); this.router = new Router({ app: this }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js index 307765e54c6..722e3f22d3e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js @@ -21,6 +21,7 @@ import $ from 'jquery'; import { union } from 'lodash'; import Backbone from 'backbone'; import Marionette from 'backbone.marionette'; +import key from 'keymaster'; import Rules from './models/rules'; import MetaView from './rule/rule-meta-view'; import DescView from './rule/rule-description-view'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js index 1e32a633a9a..b218d6939b9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import key from 'keymaster'; import WorkspaceListView from '../../components/navigator/workspace-list-view'; import WorkspaceListItemView from './workspace-list-item-view'; import WorkspaceListEmptyView from './workspace-list-empty-view'; diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js deleted file mode 100644 index 4b5abd4eb72..00000000000 --- a/server/sonar-web/src/main/js/apps/component-issues/init.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { difference } from 'lodash'; -import Backbone from 'backbone'; -import Marionette from 'backbone.marionette'; -import State from '../issues/models/state'; -import Layout from '../issues/layout'; -import Issues from '../issues/models/issues'; -import Facets from '../../components/navigator/models/facets'; -import Controller from '../issues/controller'; -import Router from '../issues/router'; -import WorkspaceListView from '../issues/workspace-list-view'; -import WorkspaceHeaderView from '../issues/workspace-header-view'; -import FacetsView from './../issues/facets-view'; -import HeaderView from './../issues/HeaderView'; - -const App = new Marionette.Application(); -const init = function({ el, component, currentUser }) { - this.config = { - resource: component.id, - resourceName: component.name, - resourceQualifier: component.qualifier - }; - this.state = new State({ - canBulkChange: currentUser.isLoggedIn, - isContext: true, - contextQuery: { componentUuids: this.config.resource }, - contextComponentUuid: this.config.resource, - contextComponentName: this.config.resourceName, - contextComponentQualifier: this.config.resourceQualifier, - contextOrganization: component.organization - }); - this.updateContextFacets(); - this.list = new Issues(); - this.facets = new Facets(); - - this.layout = new Layout({ app: this, el }); - this.layout.render(); - $('#footer').addClass('search-navigator-footer'); - - this.controller = new Controller({ app: this }); - - this.issuesView = new WorkspaceListView({ - app: this, - collection: this.list - }); - this.layout.workspaceListRegion.show(this.issuesView); - this.issuesView.bindScrollEvents(); - - this.workspaceHeaderView = new WorkspaceHeaderView({ - app: this, - collection: this.list - }); - this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView); - - this.facetsView = new FacetsView({ - app: this, - collection: this.facets - }); - this.layout.facetsRegion.show(this.facetsView); - - this.headerView = new HeaderView({ - app: this - }); - this.layout.filtersRegion.show(this.headerView); - - key.setScope('list'); - App.router = new Router({ app: App }); - Backbone.history.start(); -}; - -App.getContextQuery = function() { - return { componentUuids: this.config.resource }; -}; - -App.getRestrictedFacets = function() { - return { - TRK: ['projectUuids'], - BRC: ['projectUuids'], - DIR: ['projectUuids', 'moduleUuids', 'directories'], - DEV: ['authors'], - DEV_PRJ: ['projectUuids', 'authors'] - }; -}; - -App.updateContextFacets = function() { - const facets = this.state.get('facets'); - const allFacets = this.state.get('allFacets'); - const facetsFromServer = this.state.get('facetsFromServer'); - return this.state.set({ - facets, - allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]), - facetsFromServer: difference( - facetsFromServer, - this.getRestrictedFacets()[this.config.resourceQualifier] - ) - }); -}; - -App.on('start', options => { - init.call(App, options); -}); - -export default function(el, component, currentUser) { - App.start({ el, component, currentUser }); - - return () => { - Backbone.history.stop(); - App.layout.destroy(); - $('#footer').removeClass('search-navigator-footer'); - }; -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js index 03d857462bf..5261badeaf2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { isDiffMetric, formatLeak } from '../../utils'; import { formatMeasure } from '../../../../helpers/measures'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js index 1ccdd7eed1b..43d0d8bd422 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js @@ -19,7 +19,7 @@ */ import React from 'react'; import classNames from 'classnames'; -import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { splitPath } from '../../../../helpers/path'; import { getComponentUrl } from '../../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js deleted file mode 100644 index 1c9b447b231..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import { debounce, sortBy } from 'lodash'; -import ModalForm from '../../components/common/modal-form'; -import Template from './templates/BulkChangeForm.hbs'; -import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; -import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues'; -import { searchUsers } from '../../api/users'; -import { searchMembers } from '../../api/organizations'; -import { translate, translateWithParameters } from '../../helpers/l10n'; - -const LIMIT = 500; -const INPUT_WIDTH = '250px'; -const MINIMUM_QUERY_LENGTH = 2; -const UNASSIGNED = '<UNASSIGNED>'; - -type Issue = { - actions?: Array<string>, - assignee: string | null, - transitions?: Array<string> -}; - -const hasAction = (action: string) => - (issue: Issue) => issue.actions && issue.actions.includes(action); - -export default ModalForm.extend({ - template: Template, - - initialize() { - this.issues = null; - this.paging = null; - this.tags = null; - this.loadIssues(); - this.loadTags(); - }, - - loadIssues() { - const { query } = this.options; - searchIssues({ - ...query, - additionalFields: 'actions,transitions', - ps: LIMIT - }).then(r => { - this.issues = r.issues; - this.paging = r.paging; - this.render(); - }); - }, - - loadTags() { - searchIssueTags().then(r => { - this.tags = r.tags; - this.render(); - }); - }, - - assigneeSearch(defaultOptions) { - const { context } = this.options; - return debounce( - query => { - if (query.term.length === 0) { - query.callback({ results: defaultOptions }); - } else if (query.term.length >= MINIMUM_QUERY_LENGTH) { - const onSuccess = r => { - query.callback({ - results: r.users.map(user => ({ - id: user.login, - text: `${user.name} (${user.login})` - })) - }); - }; - if (context.isContext) { - searchMembers({ organization: context.organization, q: query.term }).then(onSuccess); - } else { - searchUsers(query.term).then(onSuccess); - } - } - }, - 250 - ); - }, - - prepareAssigneeSelect() { - const input = this.$('#assignee'); - if (input.length) { - const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues); - const currentUser = getCurrentUserFromStore(); - const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues); - const defaultOptions = []; - if (canBeAssignedToMe && currentUser.isLoggedIn) { - defaultOptions.push({ - id: currentUser.login, - text: `${currentUser.name} (${currentUser.login})` - }); - } - if (canBeUnassigned) { - defaultOptions.push({ id: UNASSIGNED, text: translate('unassigned') }); - } - - input.select2({ - allowClear: false, - placeholder: translate('search_verb'), - width: INPUT_WIDTH, - formatNoMatches: () => translate('select2.noMatches'), - formatSearching: () => translate('select2.searching'), - formatInputTooShort: () => - translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH), - query: this.assigneeSearch(defaultOptions) - }); - - input.on('change', () => this.$('#assign-action').prop('checked', true)); - } - }, - - prepareTypeSelect() { - this.$('#type') - .select2({ - minimumResultsForSearch: 999, - width: INPUT_WIDTH - }) - .on('change', () => this.$('#set-type-action').prop('checked', true)); - }, - - prepareSeveritySelect() { - const format = state => - state.id - ? `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}` - : state.text; - this.$('#severity') - .select2({ - minimumResultsForSearch: 999, - width: INPUT_WIDTH, - formatResult: format, - formatSelection: format - }) - .on('change', () => this.$('#set-severity-action').prop('checked', true)); - }, - - prepareTagsInput() { - this.$('#add_tags') - .select2({ - width: INPUT_WIDTH, - tags: this.tags - }) - .on('change', () => this.$('#add-tags-action').prop('checked', true)); - - this.$('#remove_tags') - .select2({ - width: INPUT_WIDTH, - tags: this.tags - }) - .on('change', () => this.$('#remove-tags-action').prop('checked', true)); - }, - - onRender() { - ModalForm.prototype.onRender.apply(this, arguments); - this.prepareAssigneeSelect(); - this.prepareTypeSelect(); - this.prepareSeveritySelect(); - this.prepareTagsInput(); - }, - - onFormSubmit() { - ModalForm.prototype.onFormSubmit.apply(this, arguments); - const query = {}; - - const assignee = this.$('#assignee').val(); - if (this.$('#assign-action').is(':checked') && assignee != null) { - query['assign'] = assignee === UNASSIGNED ? '' : assignee; - } - - const type = this.$('#type').val(); - if (this.$('#set-type-action').is(':checked') && type) { - query['set_type'] = type; - } - - const severity = this.$('#severity').val(); - if (this.$('#set-severity-action').is(':checked') && severity) { - query['set_severity'] = severity; - } - - const addedTags = this.$('#add_tags').val(); - if (this.$('#add-tags-action').is(':checked') && addedTags) { - query['add_tags'] = addedTags; - } - - const removedTags = this.$('#remove_tags').val(); - if (this.$('#remove-tags-action').is(':checked') && removedTags) { - query['remove_tags'] = removedTags; - } - - const transition = this.$('[name="do_transition.transition"]:checked').val(); - if (transition) { - query['do_transition'] = transition; - } - - const comment = this.$('#comment').val(); - if (comment) { - query['comment'] = comment; - } - - const sendNotifications = this.$('#send-notifications').is(':checked'); - if (sendNotifications) { - query['sendNotifications'] = sendNotifications; - } - - this.disableForm(); - this.showSpinner(); - - const issueKeys = this.issues.map(issue => issue.key); - bulkChangeIssues(issueKeys, query).then( - () => { - this.destroy(); - this.options.onChange(); - }, - (e: Object) => { - this.enableForm(); - this.hideSpinner(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - } - ); - }, - - canBeAssigned(issues: Array<Issue>) { - return issues.filter(hasAction('assign')).length; - }, - - canBeAssignedToMe(issues: Array<Issue>) { - return issues.filter(hasAction('assign_to_me')).length; - }, - - canBeUnassigned(issues: Array<Issue>) { - return issues.filter(issue => issue.assignee).length; - }, - - canChangeType(issues: Array<Issue>) { - return issues.filter(hasAction('set_type')).length; - }, - - canChangeSeverity(issues: Array<Issue>) { - return issues.filter(hasAction('set_severity')).length; - }, - - canChangeTags(issues: Array<Issue>) { - return issues.filter(hasAction('set_tags')).length; - }, - - canBeCommented(issues: Array<Issue>) { - return issues.filter(hasAction('comment')).length; - }, - - availableTransitions(issues: Array<Issue>) { - const transitions = {}; - issues.forEach(issue => { - if (issue.transitions) { - issue.transitions.forEach(t => { - if (transitions[t] != null) { - transitions[t]++; - } else { - transitions[t] = 1; - } - }); - } - }); - return sortBy(Object.keys(transitions)).map(transition => ({ - transition, - count: transitions[transition] - })); - }, - - serializeData() { - return { - ...ModalForm.prototype.serializeData.apply(this, arguments), - isLoaded: this.issues != null && this.tags != null, - issues: this.issues, - limitReached: this.paging && this.paging.total > LIMIT, - canBeAssigned: this.issues && this.canBeAssigned(this.issues), - canChangeType: this.issues && this.canChangeType(this.issues), - canChangeSeverity: this.issues && this.canChangeSeverity(this.issues), - canChangeTags: this.issues && this.canChangeTags(this.issues), - canBeCommented: this.issues && this.canBeCommented(this.issues), - availableTransitions: this.issues && this.availableTransitions(this.issues) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/HeaderView.js b/server/sonar-web/src/main/js/apps/issues/HeaderView.js deleted file mode 100644 index 596996f9d5c..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/HeaderView.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Marionette from 'backbone.marionette'; -import Template from './templates/facets/issues-my-issues-facet.hbs'; - -export default Marionette.ItemView.extend({ - template: Template, - className: 'issues-header-inner', - - events: { - 'change [name="issues-page-my"]': 'onMyIssuesChange' - }, - - initialize() { - this.listenTo(this.options.app.state, 'change:query', this.render); - }, - - onMyIssuesChange() { - const mode = this.$('[name="issues-page-my"]:checked').val(); - if (mode === 'my') { - this.options.app.state.updateFilter({ - assigned_to_me: 'true', - assignees: null, - assigned: null - }); - } else { - this.options.app.state.updateFilter({ - assigned_to_me: null, - assignees: null, - assigned: null - }); - } - }, - serializeData() { - const me = !!this.options.app.state.get('query').assigned_to_me; - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - me, - isContext: this.options.app.state.get('isContext'), - user: this.options.app.state.get('user') - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js deleted file mode 100644 index a1dcb64d7dc..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -import WithStore from '../../../components/shared/WithStore'; - -export default Marionette.ItemView.extend({ - template() { - return '<div></div>'; - }, - - initialize(options) { - this.handleLoadIssues = this.handleLoadIssues.bind(this); - this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this); - this.selectIssue = this.selectIssue.bind(this); - this.listenTo(options.app.state, 'change:selectedIndex', this.select); - }, - - onRender() { - this.showViewer(); - }, - - onDestroy() { - this.unbindShortcuts(); - unmountComponentAtNode(this.el); - }, - - handleLoadIssues(component: string) { - // TODO fromLine: number, toLine: number - const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component); - return Promise.resolve(issues); - }, - - showViewer(onLoaded) { - if (!this.baseIssue) { - return; - } - - const componentKey = this.baseIssue.get('component'); - - render( - <WithStore> - <SourceViewer - aroundLine={this.baseIssue.get('line')} - component={componentKey} - displayAllIssues={true} - loadIssues={this.handleLoadIssues} - onLoaded={onLoaded} - onIssueSelect={this.selectIssue} - selectedIssue={this.baseIssue.get('key')} - /> - </WithStore>, - this.el - ); - }, - - openFileByIssue(issue) { - this.baseIssue = issue; - this.selectedIssue = issue.get('key'); - this.showViewer(this.scrollToBaseIssue); - this.bindShortcuts(); - }, - - bindShortcuts() { - key('up', 'componentViewer', () => { - this.options.app.controller.selectPrev(); - return false; - }); - key('down', 'componentViewer', () => { - this.options.app.controller.selectNext(); - return false; - }); - key('left,backspace', 'componentViewer', () => { - this.options.app.controller.closeComponentViewer(); - return false; - }); - }, - - unbindShortcuts() { - key.deleteScope('componentViewer'); - }, - - select() { - const selected = this.options.app.state.get('selectedIndex'); - const selectedIssue = this.options.app.list.at(selected); - - if (selectedIssue.get('component') === this.baseIssue.get('component')) { - this.baseIssue = selectedIssue; - this.showViewer(this.scrollToBaseIssue); - this.scrollToBaseIssue(); - } else { - this.options.app.controller.showComponentViewer(selectedIssue); - } - }, - - scrollToLine(line) { - const row = this.$(`[data-line-number=${line}]`); - const topOffset = $(window).height() / 2 - 60; - const goal = row.length > 0 ? row.offset().top - topOffset : 0; - $(window).scrollTop(goal); - }, - - selectIssue(issueKey) { - const issue = this.options.app.list.find(model => model.get('key') === issueKey); - const index = this.options.app.list.indexOf(issue); - this.options.app.state.set({ selectedIndex: index }); - }, - - scrollToBaseIssue() { - this.scrollToLine(this.baseIssue.get('line')); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js new file mode 100644 index 00000000000..73574c611b5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -0,0 +1,649 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Helmet from 'react-helmet'; +import key from 'keymaster'; +import { keyBy, without } from 'lodash'; +import HeaderPanel from './HeaderPanel'; +import PageActions from './PageActions'; +import FiltersHeader from './FiltersHeader'; +import MyIssuesFilter from './MyIssuesFilter'; +import Sidebar from '../sidebar/Sidebar'; +import IssuesList from './IssuesList'; +import ComponentBreadcrumbs from './ComponentBreadcrumbs'; +import IssuesSourceViewer from './IssuesSourceViewer'; +import BulkChangeModal from './BulkChangeModal'; +import { + parseQuery, + areMyIssuesSelected, + areQueriesEqual, + getOpen, + serializeQuery, + parseFacets +} from '../utils'; +import type { + Query, + Paging, + Facet, + ReferencedComponent, + ReferencedUser, + ReferencedLanguage, + Component, + CurrentUser +} from '../utils'; +import ListFooter from '../../../components/controls/ListFooter'; +import EmptySearch from '../../../components/common/EmptySearch'; +import Page from '../../../components/layout/Page'; +import PageMain from '../../../components/layout/PageMain'; +import PageMainInner from '../../../components/layout/PageMainInner'; +import PageSide from '../../../components/layout/PageSide'; +import PageFilters from '../../../components/layout/PageFilters'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { scrollToElement } from '../../../helpers/scrolling'; +import type { Issue } from '../../../components/issue/types'; + +type Props = { + component?: Component, + currentUser: CurrentUser, + fetchIssues: () => Promise<*>, + location: { pathname: string, query: { [string]: string } }, + onRequestFail: (Error) => void, + router: { push: () => void, replace: () => void } +}; + +type State = { + bulkChange: 'all' | 'selected' | null, + checked: Array<string>, + facets: { [string]: Facet }, + issues: Array<Issue>, + loading: boolean, + myIssues: boolean, + openFacets: { [string]: boolean }, + paging?: Paging, + query: Query, + referencedComponents: { [string]: ReferencedComponent }, + referencedLanguages: { [string]: ReferencedLanguage }, + referencedRules: { [string]: { name: string } }, + referencedUsers: { [string]: ReferencedUser }, + selected?: string +}; + +const DEFAULT_QUERY = { resolved: 'false' }; + +export default class App extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + bulkChange: null, + checked: [], + facets: {}, + issues: [], + loading: true, + myIssues: areMyIssuesSelected(props.location.query), + openFacets: { resolutions: true, types: true }, + query: parseQuery(props.location.query), + referencedComponents: {}, + referencedLanguages: {}, + referencedRules: {}, + referencedUsers: {}, + selected: getOpen(props.location.query) + }; + } + + componentDidMount() { + this.mounted = true; + + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.add('search-navigator-footer'); + } + + this.attachShortcuts(); + this.fetchFirstIssues(); + } + + componentWillReceiveProps(nextProps: Props) { + const open = getOpen(nextProps.location.query); + if (open != null && open !== this.state.selected) { + this.setState({ selected: open }); + } + this.setState({ + myIssues: areMyIssuesSelected(nextProps.location.query), + query: parseQuery(nextProps.location.query) + }); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + const { query } = this.props.location; + const { query: prevQuery } = prevProps.location; + if ( + !areQueriesEqual(prevQuery, query) || + areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) + ) { + this.fetchFirstIssues(); + } else if (prevState.selected !== this.state.selected) { + const open = getOpen(query); + if (!open) { + this.scrollToSelectedIssue(); + } + } + } + + componentWillUnmount() { + this.detachShortcuts(); + + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.remove('search-navigator-footer'); + } + + this.mounted = false; + } + + attachShortcuts() { + key.setScope('issues'); + key('up', 'issues', () => { + this.selectPreviousIssue(); + return false; + }); + key('down', 'issues', () => { + this.selectNextIssue(); + return false; + }); + key('right', 'issues', () => { + this.openSelectedIssue(); + return false; + }); + key('left', 'issues', () => { + this.closeIssue(); + return false; + }); + } + + detachShortcuts() { + key.deleteScope('issues'); + } + + getSelectedIndex(): ?number { + const { issues, selected } = this.state; + const index = issues.findIndex(issue => issue.key === selected); + return index !== -1 ? index : null; + } + + selectNextIssue = () => { + const { issues } = this.state; + const selectedIndex = this.getSelectedIndex(); + if (issues != null && selectedIndex != null && selectedIndex < issues.length - 1) { + if (getOpen(this.props.location.query)) { + this.openIssue(issues[selectedIndex + 1].key); + } else { + this.setState({ selected: issues[selectedIndex + 1].key }); + } + } + }; + + selectPreviousIssue = () => { + const { issues } = this.state; + const selectedIndex = this.getSelectedIndex(); + if (issues != null && selectedIndex != null && selectedIndex > 0) { + if (getOpen(this.props.location.query)) { + this.openIssue(issues[selectedIndex - 1].key); + } else { + this.setState({ selected: issues[selectedIndex - 1].key }); + } + } + }; + + openIssue = (issue: string) => { + const path = { + pathname: this.props.location.pathname, + query: { + ...serializeQuery(this.state.query), + id: this.props.component && this.props.component.key, + myIssues: this.state.myIssues ? 'true' : undefined, + open: issue + } + }; + const open = getOpen(this.props.location.query); + if (open) { + this.props.router.replace(path); + } else { + this.props.router.push(path); + } + }; + + closeIssue = () => { + if (this.state.query) { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...serializeQuery(this.state.query), + id: this.props.component && this.props.component.key, + myIssues: this.state.myIssues ? 'true' : undefined, + open: undefined + } + }); + } + }; + + openSelectedIssue = () => { + const { selected } = this.state; + if (selected) { + this.openIssue(selected); + } + }; + + scrollToSelectedIssue = () => { + const { selected } = this.state; + if (selected) { + const element = document.querySelector(`[data-issue="${selected}"]`); + if (element) { + scrollToElement(element, 150, 100); + } + } + }; + + fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => { + const { component } = this.props; + const { myIssues, query } = this.state; + + const parameters = { + componentKeys: component && component.key, + ...serializeQuery(query), + s: 'FILE_LINE', + ps: 25, + facets: requestFacets + ? [ + 'assignees', + 'authors', + 'createdAt', + 'directories', + 'fileUuids', + 'languages', + 'moduleUuids', + 'projectUuids', + 'resolutions', + 'rules', + 'severities', + 'statuses', + 'tags', + 'types' + ].join() + : undefined, + ...additional + }; + + if (myIssues) { + Object.assign(parameters, { assignees: '__me__' }); + } + + return this.props.fetchIssues(parameters); + }; + + fetchFirstIssues() { + this.setState({ loading: true }); + this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => { + if (this.mounted) { + const open = getOpen(this.props.location.query); + this.setState({ + facets: parseFacets(facets), + loading: false, + issues, + paging, + referencedComponents: keyBy(other.components, 'uuid'), + referencedLanguages: keyBy(other.languages, 'key'), + referencedRules: keyBy(other.rules, 'key'), + referencedUsers: keyBy(other.users, 'login'), + selected: issues.length > 0 + ? issues.find(issue => issue.key === open) != null ? open : issues[0].key + : undefined + }); + } + }); + } + + fetchIssuesPage = (p: number): Promise<*> => { + return this.fetchIssues({ p }); + }; + + fetchIssuesUntil = (p: number, done: (Array<Issue>, Paging) => boolean) => { + return this.fetchIssuesPage(p).then(response => { + const { issues, paging } = response; + + return done(issues, paging) + ? { issues, paging } + : this.fetchIssuesUntil(p + 1, done).then(nextResponse => { + return { + issues: [...issues, ...nextResponse.issues], + paging: nextResponse.paging + }; + }); + }); + }; + + fetchMoreIssues = () => { + const { paging } = this.state; + + if (!paging) { + return; + } + + const p = paging.pageIndex + 1; + + this.setState({ loading: true }); + this.fetchIssuesPage(p).then(response => { + if (this.mounted) { + this.setState(state => ({ + loading: false, + issues: [...state.issues, ...response.issues], + paging: response.paging + })); + } + }); + }; + + fetchIssuesForComponent = (): Promise<Array<Issue>> => { + const { issues, paging } = this.state; + + const open = getOpen(this.props.location.query); + const openIssue = issues.find(issue => issue.key === open); + + if (!openIssue || !paging) { + return Promise.reject(); + } + + const isSameComponent = (issue: Issue): boolean => issue.component === openIssue.component; + + const done = (issues: Array<Issue>, paging: Paging): boolean => + paging.total <= paging.pageIndex * paging.pageSize || + issues[issues.length - 1].component !== openIssue.component; + + if (done(issues, paging)) { + return Promise.resolve(issues.filter(isSameComponent)); + } + + this.setState({ loading: true }); + return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(response => { + const nextIssues = [...issues, ...response.issues]; + + this.setState({ + issues: nextIssues, + loading: false, + paging: response.paging + }); + return nextIssues.filter(isSameComponent); + }); + }; + + isFiltered = () => { + const serialized = serializeQuery(this.state.query); + return !areQueriesEqual(serialized, DEFAULT_QUERY); + }; + + getCheckedIssues = () => { + const issues = this.state.checked.map(checked => + this.state.issues.find(issue => issue.key === checked)); + const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length }; + return Promise.resolve({ issues, paging }); + }; + + handleFilterChange = (changes: {}) => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...serializeQuery({ ...this.state.query, ...changes }), + id: this.props.component && this.props.component.key, + myIssues: this.state.myIssues ? 'true' : undefined + } + }); + }; + + handleMyIssuesChange = (myIssues: boolean) => { + this.closeFacet('assignees'); + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), + id: this.props.component && this.props.component.key, + myIssues: myIssues ? 'true' : undefined + } + }); + }; + + closeFacet = (property: string) => { + this.setState(state => ({ + openFacets: { ...state.openFacets, [property]: false } + })); + }; + + handleFacetToggle = (property: string) => { + this.setState(state => ({ + openFacets: { ...state.openFacets, [property]: !state.openFacets[property] } + })); + }; + + handleReset = () => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...DEFAULT_QUERY, + id: this.props.component && this.props.component.key, + myIssues: this.state.myIssues ? 'true' : undefined + } + }); + }; + + handleIssueCheck = (issue: string) => { + this.setState(state => ({ + checked: state.checked.includes(issue) + ? without(state.checked, issue) + : [...state.checked, issue] + })); + }; + + handleIssueChange = (issue: Issue) => { + this.setState(state => ({ + issues: state.issues.map(candidate => candidate.key === issue.key ? issue : candidate) + })); + }; + + openBulkChange = (mode: 'all' | 'selected') => { + this.setState({ bulkChange: mode }); + key.setScope('issues-bulk-change'); + }; + + closeBulkChange = () => { + key.setScope('issues'); + this.setState({ bulkChange: null }); + }; + + handleBulkChangeClick = (e: Event & { target: HTMLElement }) => { + e.preventDefault(); + e.target.blur(); + this.openBulkChange('all'); + }; + + handleBulkChangeSelectedClick = (e: Event & { target: HTMLElement }) => { + e.preventDefault(); + e.target.blur(); + this.openBulkChange('selected'); + }; + + handleBulkChangeDone = () => { + this.fetchFirstIssues(); + this.closeBulkChange(); + }; + + renderBulkChange(openIssue?: Issue) { + const { component, currentUser } = this.props; + const { bulkChange, checked, paging } = this.state; + + if (!currentUser.isLoggedIn || openIssue != null) { + return null; + } + + return ( + <div className="pull-left"> + {checked.length > 0 + ? <div className="dropdown"> + <button id="issues-bulk-change" data-toggle="dropdown"> + {translate('bulk_change')} + <i className="icon-dropdown little-spacer-left" /> + </button> + <ul className="dropdown-menu"> + <li> + <a href="#" onClick={this.handleBulkChangeClick}> + {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} + </a> + </li> + <li> + <a href="#" onClick={this.handleBulkChangeSelectedClick}> + {translateWithParameters('issues.bulk_change_selected', checked.length)} + </a> + </li> + </ul> + </div> + : <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}> + {translate('bulk_change')} + </button>} + {bulkChange != null && + <BulkChangeModal + component={component} + currentUser={currentUser} + fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues} + onClose={this.closeBulkChange} + onDone={this.handleBulkChangeDone} + onRequestFail={this.props.onRequestFail} + />} + </div> + ); + } + + renderList(openIssue?: Issue) { + const { component, currentUser } = this.props; + const { issues, paging } = this.state; + const selectedIndex = this.getSelectedIndex(); + const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null; + + if (paging == null) { + return null; + } + + return ( + <div className={openIssue != null ? 'hidden' : undefined}> + {paging.total > 0 && + <IssuesList + checked={this.state.checked} + component={component} + issues={issues} + onFilterChange={this.handleFilterChange} + onIssueChange={this.handleIssueChange} + onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined} + onIssueClick={this.openIssue} + selectedIssue={selectedIssue} + />} + + {paging.total > 0 && + <ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />} + + {paging.total === 0 && <EmptySearch />} + </div> + ); + } + + render() { + const { component, currentUser } = this.props; + const { issues, paging, query } = this.state; + + const open = getOpen(this.props.location.query); + const openIssue = issues.find(issue => issue.key === open); + + const selectedIndex = this.getSelectedIndex(); + + const top = component ? 95 : 30; + + return ( + <Page className="issues" id="issues-page"> + <Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" /> + + <PageSide top={top}> + <PageFilters> + {currentUser.isLoggedIn && + <MyIssuesFilter + myIssues={this.state.myIssues} + onMyIssuesChange={this.handleMyIssuesChange} + />} + <FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} /> + <Sidebar + component={component} + facets={this.state.facets} + myIssues={this.state.myIssues} + onFacetToggle={this.handleFacetToggle} + onFilterChange={this.handleFilterChange} + openFacets={this.state.openFacets} + query={query} + referencedComponents={this.state.referencedComponents} + referencedLanguages={this.state.referencedLanguages} + referencedRules={this.state.referencedRules} + referencedUsers={this.state.referencedUsers} + /> + </PageFilters> + </PageSide> + + <PageMain> + <HeaderPanel border={true} top={top}> + <PageMainInner> + {this.renderBulkChange(openIssue)} + {openIssue != null && + <div className="pull-left"> + <ComponentBreadcrumbs component={component} issue={openIssue} /> + </div>} + <PageActions + loading={this.state.loading} + openIssue={openIssue} + paging={paging} + selectedIndex={selectedIndex} + /> + </PageMainInner> + </HeaderPanel> + + <PageMainInner> + <div> + {openIssue != null && + <IssuesSourceViewer + openIssue={openIssue} + loadIssues={this.fetchIssuesForComponent} + onIssueChange={this.handleIssueChange} + onIssueSelect={this.openIssue} + />} + + {this.renderList(openIssue)} + </div> + </PageMainInner> + </PageMain> + </Page> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js index 061b3e8f8c6..c605961dcad 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js @@ -17,36 +17,38 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +// @flow import { connect } from 'react-redux'; -import init from '../init'; +import { withRouter } from 'react-router'; +import type { Dispatch } from 'redux'; +import App from './App'; +import { onFail } from '../../../store/rootActions'; import { getComponent, getCurrentUser } from '../../../store/rootReducer'; +import { searchIssues } from '../../../api/issues'; +import { parseIssueFromResponse } from '../../../helpers/issues'; -class ComponentIssuesAppContainer extends React.Component { - componentDidMount() { - this.stop = init(this.refs.container, this.props.component, this.props.currentUser); - } - - componentWillUnmount() { - this.stop(); - } - - render() { - // placing container inside div is required, - // because when backbone.marionette's layout is destroyed, - // it also destroys the root element, - // but react wants it to be there to unmount it - return ( - <div> - <div ref="container" /> - </div> - ); - } -} +type Query = { [string]: string }; const mapStateToProps = (state, ownProps) => ({ - component: getComponent(state, ownProps.location.query.id), + component: ownProps.location.query.id + ? getComponent(state, ownProps.location.query.id) + : undefined, currentUser: getCurrentUser(state) }); -export default connect(mapStateToProps)(ComponentIssuesAppContainer); +const fetchIssues = (query: Query) => + (dispatch: Dispatch<*>) => + searchIssues({ ...query, additionalFields: '_all' }).then( + response => { + const parsedIssues = response.issues.map(issue => + parseIssueFromResponse(issue, response.components, response.users, response.rules)); + return { ...response, issues: parsedIssues }; + }, + onFail(dispatch) + ); + +const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error); + +const mapDispatchToProps = { fetchIssues, onRequestFail }; + +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App)); diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js new file mode 100644 index 00000000000..3b7d454aa72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js @@ -0,0 +1,522 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import Select from 'react-select'; +import { css } from 'glamor'; +import { pickBy, sortBy } from 'lodash'; +import SearchSelect from './SearchSelect'; +import Checkbox from '../../../components/controls/Checkbox'; +import Tooltip from '../../../components/controls/Tooltip'; +import MarkdownTips from '../../../components/common/MarkdownTips'; +import SeverityHelper from '../../../components/shared/SeverityHelper'; +import Avatar from '../../../components/ui/Avatar'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import { searchIssueTags, bulkChangeIssues } from '../../../api/issues'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { searchAssignees } from '../utils'; +import type { Paging, Component, CurrentUser } from '../utils'; +import type { Issue } from '../../../components/issue/types'; + +type Props = {| + component?: Component, + currentUser: CurrentUser, + fetchIssues: ({}) => Promise<*>, + onClose: () => void, + onDone: () => void, + onRequestFail: (Error) => void +|}; + +type State = {| + issues: Array<Issue>, + // used for initial loading of issues + loading: boolean, + paging?: Paging, + // used when submitting a form + submitting: boolean, + tags?: Array<string>, + + // form fields + addTags?: Array<string>, + assignee?: string, + comment?: string, + notifications?: boolean, + removeTags?: Array<string>, + severity?: string, + transition?: string, + type?: string +|}; + +const hasAction = (action: string) => + (issue: Issue): boolean => issue.actions && issue.actions.includes(action); + +export default class BulkChangeModal extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { issues: [], loading: true, submitting: false }; + } + + componentDidMount() { + this.mounted = true; + Promise.all([this.loadIssues(), searchIssueTags()]).then(([issues, tags]) => { + if (this.mounted) { + this.setState({ + issues: issues.issues, + loading: false, + paging: issues.paging, + tags + }); + } + }); + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCloseClick = (e: Event & { target: HTMLElement }) => { + e.preventDefault(); + e.target.blur(); + this.props.onClose(); + }; + + loadIssues = () => { + return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 }); + }; + + handleAssigneeSearch = (query: string) => { + if (query.length > 1) { + return searchAssignees(query, this.props.component); + } else { + const { currentUser } = this.props; + const { issues } = this.state; + const options = []; + + if (currentUser.isLoggedIn) { + const canBeAssignedToMe = issues.filter( + issue => issue.assignee !== currentUser.login + ).length > 0; + if (canBeAssignedToMe) { + options.push({ + email: currentUser.email, + label: currentUser.name, + value: currentUser.login + }); + } + } + + const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0; + if (canBeUnassigned) { + options.push({ label: translate('unassigned'), value: '' }); + } + + return Promise.resolve(options); + } + }; + + handleAssigneeSelect = (assignee: string) => { + this.setState({ assignee }); + }; + + handleFieldCheck = (field: string) => + (checked: boolean) => { + if (!checked) { + this.setState({ [field]: undefined }); + } else if (field === 'notifications') { + this.setState({ [field]: true }); + } + }; + + handleFieldChange = (field: string) => + (event: { target: HTMLInputElement }) => { + this.setState({ [field]: event.target.value }); + }; + + handleSelectFieldChange = (field: string) => + ({ value }: { value: string }) => { + this.setState({ [field]: value }); + }; + + handleMultiSelectFieldChange = (field: string) => + (options: Array<{ value: string }>) => { + this.setState({ [field]: options.map(option => option.value) }); + }; + + handleSubmit = (e: Event) => { + e.preventDefault(); + const query = pickBy({ + assign: this.state.assignee, + set_type: this.state.type, + set_severity: this.state.severity, + add_tags: this.state.addTags && this.state.addTags.join(), + remove_tags: this.state.removeTags && this.state.removeTags.join(), + do_transition: this.state.transition, + comment: this.state.comment, + sendNotifications: this.state.notifications + }); + const issueKeys = this.state.issues.map(issue => issue.key); + + this.setState({ submitting: true }); + bulkChangeIssues(issueKeys, query).then( + () => { + this.setState({ submitting: false }); + this.props.onDone(); + }, + (error: Error) => { + this.setState({ submitting: false }); + this.props.onRequestFail(error); + } + ); + }; + + getAvailableTransitions(issues: Array<Issue>): Array<{ transition: string, count: number }> { + const transitions = {}; + issues.forEach(issue => { + if (issue.transitions) { + issue.transitions.forEach(t => { + if (transitions[t] != null) { + transitions[t]++; + } else { + transitions[t] = 1; + } + }); + } + }); + return sortBy(Object.keys(transitions)).map(transition => ({ + transition, + count: transitions[transition] + })); + } + + renderCancelButton = () => ( + <a id="bulk-change-cancel" href="#" onClick={this.handleCloseClick}> + {translate('cancel')} + </a> + ); + + renderLoading = () => ( + <div> + <div className="modal-head"> + <h2>{translate('bulk_change')}</h2> + </div> + <div className="modal-body"> + <div className="text-center"> + <i className="spinner spinner-margin" /> + </div> + </div> + <div className="modal-foot"> + {this.renderCancelButton()} + </div> + </div> + ); + + renderCheckbox = (field: string) => ( + <Checkbox + className={css({ paddingTop: 6, paddingRight: 8 })} + checked={this.state[field] != null} + onCheck={this.handleFieldCheck(field)} + /> + ); + + renderAffected = (affected: number) => ( + <div className="pull-right note"> + ({translateWithParameters('issue_bulk_change.x_issues', affected)}) + </div> + ); + + renderField = (field: string, label: string, affected: ?number, input: Object) => ( + <div className="modal-field" id={`issues-bulk-change-${field}`}> + <label htmlFor={field}>{translate(label)}</label> + {this.renderCheckbox(field)} + {input} + {affected != null && this.renderAffected(affected)} + </div> + ); + + renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => ( + <span> + {(option.avatar != null || option.email != null) && + <Avatar + className="little-spacer-right" + email={option.email} + hash={option.avatar} + size={16} + />} + {option.label} + </span> + ); + + renderAssigneeField = () => { + const affected: number = this.state.issues.filter(hasAction('assign')).length; + + if (affected === 0) { + return null; + } + + const input = ( + <SearchSelect + onSearch={this.handleAssigneeSearch} + onSelect={this.handleAssigneeSelect} + minimumQueryLength={0} + renderOption={this.renderAssigneeOption} + resetOnBlur={false} + value={this.state.assignee} + /> + ); + + return this.renderField('assignee', 'issue.assign.formlink', affected, input); + }; + + renderTypeField = () => { + const affected: number = this.state.issues.filter(hasAction('set_type')).length; + + if (affected === 0) { + return null; + } + + const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; + const options = types.map(type => ({ label: translate('issue.type', type), value: type })); + + const optionRenderer = (option: { label: string, value: string }) => ( + <span> + <IssueTypeIcon className="little-spacer-right" query={option.value} /> + {option.label} + </span> + ); + + const input = ( + <Select + clearable={false} + id="type" + onChange={this.handleSelectFieldChange('type')} + options={options} + optionRenderer={optionRenderer} + searchable={false} + value={this.state.type} + valueRenderer={optionRenderer} + /> + ); + + return this.renderField('type', 'issue.set_type', affected, input); + }; + + renderSeverityField = () => { + const affected: number = this.state.issues.filter(hasAction('set_severity')).length; + + if (affected === 0) { + return null; + } + + const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; + const options = severities.map(severity => ({ + label: translate('severity', severity), + value: severity + })); + + const input = ( + <Select + clearable={false} + id="severity" + onChange={this.handleSelectFieldChange('severity')} + options={options} + optionRenderer={option => <SeverityHelper severity={option.value} />} + searchable={false} + value={this.state.severity} + valueRenderer={option => <SeverityHelper severity={option.value} />} + /> + ); + + return this.renderField('severity', 'issue.set_severity', affected, input); + }; + + renderAddTagsField = () => { + const affected: number = this.state.issues.filter(hasAction('set_tags')).length; + + if (this.state.tags == null || affected === 0) { + return null; + } + + const options = this.state.tags.map(tag => ({ label: tag, value: tag })); + + const input = ( + <Select + clearable={false} + id="add_tags" + multi={true} + onChange={this.handleMultiSelectFieldChange('addTags')} + options={options} + searchable={true} + value={this.state.addTags} + /> + ); + + return this.renderField('addTags', 'issue.add_tags', affected, input); + }; + + renderRemoveTagsField = () => { + const affected: number = this.state.issues.filter(hasAction('set_tags')).length; + + if (this.state.tags == null || affected === 0) { + return null; + } + + const options = this.state.tags.map(tag => ({ label: tag, value: tag })); + + const input = ( + <Select + clearable={false} + id="remove_tags" + multi={true} + onChange={this.handleMultiSelectFieldChange('removeTags')} + options={options} + searchable={true} + value={this.state.removeTags} + /> + ); + + return this.renderField('removeTags', 'issue.remove_tags', affected, input); + }; + + renderTransitionsField = () => { + const transitions = this.getAvailableTransitions(this.state.issues); + + if (transitions.length === 0) { + return null; + } + + return ( + <div className="modal-field"> + <label>{translate('issue.transition')}</label> + {transitions.map(transition => ( + <span key={transition.transition}> + <input + checked={this.state.transition === transition.transition} + id={`transition-${transition.transition}`} + name="do_transition.transition" + onChange={this.handleFieldChange('transition')} + type="radio" + value={transition.transition} + /> + <label + htmlFor={`transition-${transition.transition}`} + style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}> + {translate('issue.transition', transition.transition)} + </label> + {this.renderAffected(transition.count)} + <br /> + </span> + ))} + </div> + ); + }; + + renderCommentField = () => { + const affected: number = this.state.issues.filter(hasAction('comment')).length; + + if (affected === 0) { + return null; + } + + return ( + <div className="modal-field"> + <label htmlFor="comment"> + {translate('issue.comment.formlink')} + <Tooltip overlay={translate('issue_bulk_change.comment.help')}> + <i className="icon-help little-spacer-left" /> + </Tooltip> + </label> + <div> + <textarea + id="comment" + onChange={this.handleFieldChange('comment')} + rows="4" + style={{ width: '100%' }} + value={this.state.comment || ''} + /> + </div> + <div className="pull-right"> + <MarkdownTips /> + </div> + </div> + ); + }; + + renderNotificationsField = () => ( + <div className="modal-field"> + <label htmlFor="send-notifications">{translate('issue.send_notifications')}</label> + {this.renderCheckbox('notifications')} + </div> + ); + + renderForm = () => { + const { issues, paging, submitting } = this.state; + + const limitReached: boolean = paging != null && + paging.total > paging.pageIndex * paging.pageSize; + + return ( + <form id="bulk-change-form" onSubmit={this.handleSubmit}> + <div className="modal-head"> + <h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2> + </div> + + <div className="modal-body"> + {limitReached && + <div className="alert alert-warning"> + {translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)} + </div>} + + {this.renderAssigneeField()} + {this.renderTypeField()} + {this.renderSeverityField()} + {this.renderAddTagsField()} + {this.renderRemoveTagsField()} + {this.renderTransitionsField()} + {this.renderCommentField()} + {this.renderNotificationsField()} + </div> + + <div className="modal-foot"> + {submitting && <i className="spinner spacer-right" />} + <button disabled={submitting} id="bulk-change-submit">{translate('apply')}</button> + {this.renderCancelButton()} + </div> + </form> + ); + }; + + render() { + return ( + <Modal + isOpen={true} + contentLabel="modal" + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + {this.state.loading ? this.renderLoading() : this.renderForm()} + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js new file mode 100644 index 00000000000..bc12da43bf1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { Link } from 'react-router'; +import Organization from '../../../components/shared/Organization'; +import { collapsePath, limitComponentName } from '../../../helpers/path'; +import { getProjectUrl } from '../../../helpers/urls'; +import type { Component } from '../utils'; + +type Props = { + component?: Component, + issue: Object +}; + +export default class ComponentBreadcrumbs extends React.PureComponent { + props: Props; + + render() { + const { component, issue } = this.props; + + const displayOrganization = component == null || ['VW', 'SVW'].includes(component.qualifier); + const displayProject = component == null || + !['TRK', 'BRC', 'DIR'].includes(component.qualifier); + const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier); + + return ( + <div className="component-name"> + {displayOrganization && + <Organization linkClassName="link-no-underline" organizationKey={issue.organization} />} + + {displayProject && + <span> + <Link to={getProjectUrl(issue.project)} className="link-no-underline"> + {limitComponentName(issue.projectName)} + </Link> + <span className="slash-separator" /> + </span>} + + {displaySubProject && + issue.subProject != null && + <span> + <Link to={getProjectUrl(issue.subProject)} className="link-no-underline"> + {limitComponentName(issue.subProjectName)} + </Link> + <span className="slash-separator" /> + </span>} + + <Link to={getProjectUrl(issue.component)} className="link-no-underline"> + {collapsePath(issue.componentLongName)} + </Link> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js index b1f3e70f3f0..9740f63e718 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js @@ -17,39 +17,39 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +// @flow import React from 'react'; -import { connect } from 'react-redux'; -import init from '../init'; -import { getCurrentUser } from '../../../store/rootReducer'; +import { css } from 'glamor'; +import { translate } from '../../../helpers/l10n'; -class IssuesAppContainer extends React.Component { - static propTypes = { - currentUser: React.PropTypes.any.isRequired - }; +type Props = { + displayReset: boolean, + onReset: () => void +}; - componentDidMount() { - this.stop = init(this.refs.container, this.props.currentUser); - } +const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' }); - componentWillUnmount() { - this.stop(); - } +export default class FiltersHeader extends React.PureComponent { + props: Props; + + handleResetClick = (e: Event & { currentTarget: HTMLElement }) => { + e.preventDefault(); + e.currentTarget.blur(); + this.props.onReset(); + }; render() { - // placing container inside div is required, - // because when backbone.marionette's layout is destroyed, - // it also destroys the root element, - // but react wants it to be there to unmount it return ( - <div> - <div ref="container" /> + <div className={styles}> + {this.props.displayReset && + <div className={css({ float: 'right' })}> + <button className="button-red" onClick={this.handleResetClick}> + {translate('clear_all_filters')} + </button> + </div>} + + <h3>{translate('filters')}</h3> </div> ); } } - -const mapStateToProps = state => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(IssuesAppContainer); diff --git a/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js new file mode 100644 index 00000000000..2d8530dab51 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css, media } from 'glamor'; +import { clearfix } from 'glamor/utils'; +import { throttle } from 'lodash'; + +type Props = {| + border: boolean, + children?: React.Element<*>, + top?: number +|}; + +type State = { + scrolled: boolean +}; + +export default class HeaderPanel extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { scrolled: this.isScrolled() }; + this.handleScroll = throttle(this.handleScroll, 50); + } + + componentDidMount() { + if (this.props.top != null) { + window.addEventListener('scroll', this.handleScroll); + } + } + + componentWillUnmount() { + if (this.props.top != null) { + window.removeEventListener('scroll', this.handleScroll); + } + } + + isScrolled = () => window.scrollY > 10; + + handleScroll = () => { + this.setState({ scrolled: this.isScrolled() }); + }; + + render() { + const commonStyles = { + height: 56, + lineHeight: '24px', + padding: '16px 20px', + boxSizing: 'border-box', + borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined, + backgroundColor: '#f3f3f3' + }; + + const inner = this.props.top + ? <div + className={css( + commonStyles, + { + position: 'fixed', + zIndex: 30, + top: this.props.top, + left: 'calc(50vw - 360px + 1px)', + right: 0, + boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none', + transition: 'box-shadow 0.3s ease' + }, + media('(max-width: 1320px)', { left: 301 }) + )}> + {this.props.children} + </div> + : this.props.children; + + return ( + <div + className={css(clearfix(), commonStyles, { + marginTop: -20, + marginBottom: 20, + marginLeft: -20, + marginRight: -20, + '& .component-name': { lineHeight: '24px' } + })}> + {inner} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js new file mode 100644 index 00000000000..4bd9cf945d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import ListItem from './ListItem'; +import type { Issue } from '../../../components/issue/types'; +import type { Component } from '../utils'; + +type Props = {| + checked: Array<string>, + component?: Component, + issues: Array<Issue>, + onFilterChange: (changes: {}) => void, + onIssueChange: (Issue) => void, + onIssueCheck?: (string) => void, + onIssueClick: (string) => void, + selectedIssue: ?Issue +|}; + +export default class IssuesList extends React.PureComponent { + props: Props; + + render() { + const { checked, component, issues, selectedIssue } = this.props; + + return ( + <div> + {issues.map((issue, index) => ( + <ListItem + checked={checked.includes(issue.key)} + component={component} + key={issue.key} + issue={issue} + onChange={this.props.onIssueChange} + onCheck={this.props.onIssueCheck} + onClick={this.props.onIssueClick} + onFilterChange={this.props.onFilterChange} + previousIssue={index > 0 ? issues[index - 1] : null} + selected={selectedIssue != null && selectedIssue.key === issue.key} + /> + ))} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js new file mode 100644 index 00000000000..ff090ed8287 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import SourceViewer from '../../../components/SourceViewer/SourceViewer'; +import { scrollToElement } from '../../../helpers/scrolling'; +import type { Issue } from '../../../components/issue/types'; + +type Props = {| + loadIssues: () => Promise<*>, + onIssueChange: (Issue) => void, + onIssueSelect: (string) => void, + openIssue: Issue +|}; + +export default class IssuesSourceViewer extends React.PureComponent { + node: HTMLElement; + props: Props; + + componentDidUpdate(prevProps: Props) { + if (prevProps.openIssue.component === this.props.openIssue.component) { + this.scrollToIssue(); + } + } + + scrollToIssue = () => { + const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`); + if (element) { + scrollToElement(element, 100, 100); + } + }; + + render() { + const { openIssue } = this.props; + + return ( + <div ref={node => this.node = node}> + <SourceViewer + aroundLine={openIssue.line} + component={openIssue.component} + displayAllIssues={true} + loadIssues={this.props.loadIssues} + onLoaded={this.scrollToIssue} + onIssueChange={this.props.onIssueChange} + onIssueSelect={this.props.onIssueSelect} + selectedIssue={openIssue.key} + /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js new file mode 100644 index 00000000000..f8a0bdda14a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import ComponentBreadcrumbs from './ComponentBreadcrumbs'; +import Issue from '../../../components/issue/Issue'; +import type { Issue as IssueType } from '../../../components/issue/types'; +import type { Component } from '../utils'; + +type Props = {| + checked: boolean, + component?: Component, + issue: IssueType, + onChange: (IssueType) => void, + onCheck?: (string) => void, + onClick: (string) => void, + onFilterChange: (changes: {}) => void, + previousIssue: ?Object, + selected: boolean +|}; + +type State = { + similarIssues: boolean +}; + +export default class ListItem extends React.PureComponent { + props: Props; + state: State = { similarIssues: false }; + + handleFilter = (property: string, issue: IssueType) => { + const { onFilterChange } = this.props; + + const issuesReset = { issues: [] }; + + if (property.startsWith('tag###')) { + const tag = property.substr(6); + return onFilterChange({ ...issuesReset, tags: [tag] }); + } + + switch (property) { + case 'type': + return onFilterChange({ ...issuesReset, types: [issue.type] }); + case 'severity': + return onFilterChange({ ...issuesReset, severities: [issue.severity] }); + case 'status': + return onFilterChange({ ...issuesReset, statuses: [issue.status] }); + case 'resolution': + return issue.resolution != null + ? onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] }) + : onFilterChange({ ...issuesReset, resolved: false, resolutions: [] }); + case 'assignee': + return issue.assignee != null + ? onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] }) + : onFilterChange({ ...issuesReset, assigned: false, assignees: [] }); + case 'rule': + return onFilterChange({ ...issuesReset, rules: [issue.rule] }); + case 'project': + return onFilterChange({ ...issuesReset, projects: [issue.projectUuid] }); + case 'module': + return onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] }); + case 'file': + return onFilterChange({ ...issuesReset, files: [issue.componentUuid] }); + } + }; + + render() { + const { component, issue, previousIssue } = this.props; + + const displayComponent = previousIssue == null || previousIssue.component !== issue.component; + + return ( + <div className="issues-workspace-list-item"> + {displayComponent && + <div className="issues-workspace-list-component"> + <ComponentBreadcrumbs component={component} issue={this.props.issue} /> + </div>} + <Issue + checked={this.props.checked} + issue={issue} + onChange={this.props.onChange} + onCheck={this.props.onCheck} + onClick={this.props.onClick} + onFilter={this.handleFilter} + selected={this.props.selected} + /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js new file mode 100644 index 00000000000..a8f6441b1fd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css } from 'glamor'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + myIssues: boolean, + onMyIssuesChange: (boolean) => void +|}; + +export default class MyIssuesFilter extends React.Component { + props: Props; + + handleClick = (myIssues: boolean) => + (e: Event & { currentTarget: HTMLElement }) => { + e.preventDefault(); + e.currentTarget.blur(); + this.props.onMyIssuesChange(myIssues); + }; + + render() { + const { myIssues } = this.props; + + return ( + <div className={css({ marginBottom: 24, textAlign: 'center' })}> + <div className="button-group"> + <button + className={myIssues ? 'button-active' : undefined} + onClick={this.handleClick(true)}> + {translate('issues.my_issues')} + </button> + <button + className={myIssues ? undefined : 'button-active'} + onClick={this.handleClick(false)}> + {translate('all')} + </button> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js new file mode 100644 index 00000000000..262c3ad8699 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { css } from 'glamor'; +import type { Paging } from '../utils'; +import { translate } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; + +type Props = {| + loading: boolean, + openIssue: ?{}, + paging: ?Paging, + selectedIndex: ?number +|}; + +export default class PageActions extends React.Component { + props: Props; + + renderShortcuts() { + return ( + <span className="note big-spacer-right"> + <span className="big-spacer-right"> + <span className="shortcut-button little-spacer-right">↑</span> + <span className="shortcut-button little-spacer-right">↓</span> + {translate('issues.to_select_issues')} + </span> + + <span> + <span className="shortcut-button little-spacer-right">←</span> + <span className="shortcut-button little-spacer-right">→</span> + {translate('issues.to_navigate')} + </span> + </span> + ); + } + + render() { + const { openIssue, paging, selectedIndex } = this.props; + + return ( + <div className={css({ float: 'right' })}> + {openIssue == null && this.renderShortcuts()} + + <div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}> + {this.props.loading && <i className="spinner spacer-right" />} + {paging != null && + <span> + <strong> + {selectedIndex != null && <span>{selectedIndex + 1} / </span>} + {formatMeasure(paging.total, 'INT')} + </strong> + {' '} + {translate('issues.issues')} + </span>} + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js new file mode 100644 index 00000000000..eddea6983ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Select from 'react-select'; +import { debounce } from 'lodash'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +type Option = { label: string, value: string }; + +type Props = {| + minimumQueryLength: number, + onSearch: (query: string) => Promise<Array<Option>>, + onSelect: (value: string) => void, + renderOption?: (option: Object) => React.Element<*>, + resetOnBlur: boolean, + value?: string +|}; + +type State = { + loading: boolean, + options: Array<Option>, + query: string +}; + +export default class SearchSelect extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + static defaultProps = { + minimumQueryLength: 2, + resetOnBlur: true + }; + + constructor(props: Props) { + super(props); + this.state = { loading: false, options: [], query: '' }; + this.search = debounce(this.search, 250); + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + search = (query: string) => { + this.props.onSearch(query).then(options => { + if (this.mounted) { + this.setState({ loading: false, options }); + } + }); + }; + + handleBlur = () => { + this.setState({ options: [], query: '' }); + }; + + handleChange = (option: Option) => { + this.props.onSelect(option.value); + }; + + handleInputChange = (query: string = '') => { + if (query.length >= this.props.minimumQueryLength) { + this.setState({ loading: true, query }); + this.search(query); + } else { + this.setState({ options: [], query }); + } + }; + + // disable internal filtering + handleFilterOption = () => true; + + render() { + return ( + <Select + autofocus={true} + cache={false} + className="input-super-large" + clearable={false} + filterOption={this.handleFilterOption} + isLoading={this.state.loading} + noResultsText={ + this.state.query.length < this.props.minimumQueryLength + ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength) + : translate('select2.noMatches') + } + onBlur={this.props.resetOnBlur ? this.handleBlur : undefined} + onChange={this.handleChange} + onInputChange={this.handleInputChange} + onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined} + optionRenderer={this.props.renderOption} + options={this.state.options} + placeholder={translate('search_verb')} + searchable={true} + value={this.props.value} + valueRenderer={this.props.renderOption} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js new file mode 100644 index 00000000000..f4d46d4f8a8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import SearchSelect from '../SearchSelect'; + +jest.mock('lodash', () => ({ + debounce: fn => fn +})); + +it('should render Select', () => { + expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot(); +}); + +it('should call onSelect', () => { + const onSelect = jest.fn(); + const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />); + wrapper.prop('onChange')({ value: 'foo' }); + expect(onSelect).lastCalledWith('foo'); +}); + +it('should call onSearch', () => { + const onSearch = jest.fn().mockReturnValue(Promise.resolve([])); + const wrapper = shallow( + <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} /> + ); + wrapper.prop('onInputChange')('f'); + expect(onSearch).not.toHaveBeenCalled(); + wrapper.prop('onInputChange')('foo'); + expect(onSearch).lastCalledWith('foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap new file mode 100644 index 00000000000..4f2fe37cc62 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap @@ -0,0 +1,48 @@ +exports[`test should render Select 1`] = ` +<Select + addLabelText="Add \"{label}\"?" + arrowRenderer={[Function]} + autofocus={true} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + cache={false} + className="input-super-large" + clearAllText="Clear all" + clearValueText="Clear value" + clearable={false} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOption={[Function]} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="select2.tooShort.2" + onBlur={[Function]} + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + onInputChange={[Function]} + openAfterFocus={false} + optionComponent={[Function]} + options={Array []} + pageSize={5} + placeholder="search_verb" + required={false} + scrollMenuIntoView={true} + searchable={true} + simpleValue={false} + tabSelectsValue={true} + valueComponent={[Function]} + valueKey="value" /> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js deleted file mode 100644 index f98f292d4bd..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/controller.js +++ /dev/null @@ -1,217 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import Backbone from 'backbone'; -import Controller from '../../components/navigator/controller'; -import ComponentViewer from './component-viewer/main'; -import getStore from '../../app/utils/getStore'; -import { receiveIssues } from '../../store/issues/duck'; - -const FACET_DATA_FIELDS = ['components', 'users', 'rules', 'languages']; - -export default Controller.extend({ - _issuesParameters() { - return { - p: this.options.app.state.get('page'), - ps: this.pageSize, - asc: true, - additionalFields: '_all', - facets: this._facetsFromServer().join() - }; - }, - - receiveIssues(issues) { - const store = getStore(); - store.dispatch(receiveIssues(issues)); - }, - - fetchList(firstPage) { - const that = this; - if (firstPage == null) { - firstPage = true; - } - if (firstPage) { - this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true }); - this.closeComponentViewer(); - } - const data = this._issuesParameters(); - Object.assign(data, this.options.app.state.get('query')); - if (this.options.app.state.get('query').assigned_to_me) { - Object.assign(data, { assignees: '__me__' }); - } - if (this.options.app.state.get('isContext')) { - Object.assign(data, this.options.app.state.get('contextQuery')); - } - return $.get(window.baseUrl + '/api/issues/search', data).done(r => { - const issues = that.options.app.list.parseIssues(r); - this.receiveIssues(issues); - if (firstPage) { - const issues = that.options.app.list.parseIssues(r); - that.options.app.list.reset(issues); - } else { - const issues = that.options.app.list.parseIssues(r, that.options.app.list.length); - that.options.app.list.add(issues); - } - that.options.app.list.setIndex(); - FACET_DATA_FIELDS.forEach(field => { - that.options.app.facets[field] = r[field]; - }); - that.options.app.facets.reset(that._allFacets()); - that.options.app.facets.add(r.facets, { merge: true }); - that.enableFacets(that._enabledFacets()); - if (firstPage) { - that.options.app.state.set({ - page: r.p, - pageSize: r.ps, - total: r.total, - maxResultsReached: r.p * r.ps >= r.total - }); - } else { - that.options.app.state.set({ - page: r.p, - maxResultsReached: r.p * r.ps >= r.total - }); - } - if (firstPage && that.isIssuePermalink()) { - that.showComponentViewer(that.options.app.list.first()); - } - }); - }, - - isIssuePermalink() { - const query = this.options.app.state.get('query'); - return query.issues != null && this.options.app.list.length === 1; - }, - - _mergeCollections(a, b) { - const collection = new Backbone.Collection(a); - collection.add(b, { merge: true }); - return collection.toJSON(); - }, - - requestFacet(id) { - const that = this; - const facet = this.options.app.facets.get(id); - const data = { - facets: id, - ps: 1, - additionalFields: '_all', - ...this.options.app.state.get('query') - }; - if (this.options.app.state.get('query').assigned_to_me) { - Object.assign(data, { assignees: '__me__' }); - } - if (this.options.app.state.get('isContext')) { - Object.assign(data, this.options.app.state.get('contextQuery')); - } - return $.get(window.baseUrl + '/api/issues/search', data, r => { - FACET_DATA_FIELDS.forEach(field => { - that.options.app.facets[field] = that._mergeCollections( - that.options.app.facets[field], - r[field] - ); - }); - const facetData = r.facets.find(facet => facet.property === id); - if (facetData != null) { - facet.set(facetData); - } - }); - }, - - newSearch() { - this.options.app.state.unset('filter'); - return this.options.app.state.setQuery({ resolved: 'false' }); - }, - - parseQuery() { - const q = Controller.prototype.parseQuery.apply(this, arguments); - delete q.asc; - delete q.s; - delete q.id; - return q; - }, - - getQueryAsObject() { - const state = this.options.app.state; - const query = state.get('query'); - if (query.assigned_to_me) { - Object.assign(query, { assignees: '__me__' }); - } - if (state.get('isContext')) { - Object.assign(query, state.get('contextQuery')); - } - return query; - }, - - getQuery(separator, addContext, handleMyIssues = false) { - if (separator == null) { - separator = '|'; - } - if (addContext == null) { - addContext = false; - } - const filter = this.options.app.state.get('query'); - if (addContext && this.options.app.state.get('isContext')) { - Object.assign(filter, this.options.app.state.get('contextQuery')); - } - if (handleMyIssues && this.options.app.state.get('query').assigned_to_me) { - Object.assign(filter, { assignees: '__me__' }); - } - const route = []; - Object.keys(filter).forEach(property => { - route.push(`${property}=${encodeURIComponent(filter[property])}`); - }); - return route.join(separator); - }, - - _prepareComponent(issue) { - return { - key: issue.get('component'), - name: issue.get('componentLongName'), - qualifier: issue.get('componentQualifier'), - subProject: issue.get('subProject'), - subProjectName: issue.get('subProjectLongName'), - project: issue.get('project'), - projectName: issue.get('projectLongName'), - projectOrganization: issue.get('projectOrganization') - }; - }, - - showComponentViewer(issue) { - this.options.app.layout.workspaceComponentViewerRegion.reset(); - key.setScope('componentViewer'); - this.options.app.issuesView.unbindScrollEvents(); - this.options.app.state.set('component', this._prepareComponent(issue)); - this.options.app.componentViewer = new ComponentViewer({ app: this.options.app }); - this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer); - this.options.app.layout.showComponentViewer(); - return this.options.app.componentViewer.openFileByIssue(issue); - }, - - closeComponentViewer() { - key.setScope('list'); - $('body').click(); - this.options.app.state.unset('component'); - this.options.app.layout.workspaceComponentViewerRegion.reset(); - this.options.app.layout.hideComponentViewer(); - this.options.app.issuesView.bindScrollEvents(); - return this.options.app.issuesView.scrollTo(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets-view.js b/server/sonar-web/src/main/js/apps/issues/facets-view.js deleted file mode 100644 index f706ef53a39..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets-view.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import FacetsView from '../../components/navigator/facets-view'; -import BaseFacet from './facets/base-facet'; -import TypeFacet from './facets/type-facet'; -import SeverityFacet from './facets/severity-facet'; -import StatusFacet from './facets/status-facet'; -import ProjectFacet from './facets/project-facet'; -import ModuleFacet from './facets/module-facet'; -import AssigneeFacet from './facets/assignee-facet'; -import RuleFacet from './facets/rule-facet'; -import TagFacet from './facets/tag-facet'; -import ResolutionFacet from './facets/resolution-facet'; -import CreationDateFacet from './facets/creation-date-facet'; -import FileFacet from './facets/file-facet'; -import LanguageFacet from './facets/language-facet'; -import AuthorFacet from './facets/author-facet'; -import IssueKeyFacet from './facets/issue-key-facet'; -import ContextFacet from './facets/context-facet'; -import ModeFacet from './facets/mode-facet'; - -const viewsMapping = { - types: TypeFacet, - severities: SeverityFacet, - statuses: StatusFacet, - assignees: AssigneeFacet, - resolutions: ResolutionFacet, - createdAt: CreationDateFacet, - projectUuids: ProjectFacet, - moduleUuids: ModuleFacet, - rules: RuleFacet, - tags: TagFacet, - fileUuids: FileFacet, - languages: LanguageFacet, - authors: AuthorFacet, - issues: IssueKeyFacet, - context: ContextFacet, - facetMode: ModeFacet -}; - -export default FacetsView.extend({ - getChildView(model) { - const view = viewsMapping[model.get('property')]; - return view ? view : BaseFacet; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js deleted file mode 100644 index 597afcaee47..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { sortBy } from 'lodash'; -import CustomValuesFacet from './custom-values-facet'; -import Template from '../templates/facets/issues-assignee-facet.hbs'; - -export default CustomValuesFacet.extend({ - template: Template, - - initialize() { - this.context = { - isContext: this.options.app.state.get('isContext'), - organization: this.options.app.state.get('contextOrganization') - }; - }, - - getUrl() { - return window.baseUrl + - (this.context.isContext ? '/api/organizations/search_members' : '/api/users/search'); - }, - - prepareAjaxSearch() { - return { - quietMillis: 300, - url: this.getUrl(), - data: (term, page) => { - if (this.context.isContext && this.context.organization) { - return { q: term, p: page, organization: this.context.organization }; - } else { - return { q: term, p: page }; - } - }, - results: window.usersToSelect2 - }; - }, - - onRender() { - CustomValuesFacet.prototype.onRender.apply(this, arguments); - - const myIssuesSelected = !!this.options.app.state.get('query').assigned_to_me; - this.$el.toggleClass('hidden', myIssuesSelected); - - const value = this.options.app.state.get('query').assigned; - if (value != null && (!value || value === 'false')) { - this.$('.js-facet').filter('[data-unassigned]').addClass('active'); - } - }, - - toggleFacet(e) { - const unassigned = $(e.currentTarget).is('[data-unassigned]'); - $(e.currentTarget).toggleClass('active'); - if (unassigned) { - const checked = $(e.currentTarget).is('.active'); - const value = checked ? 'false' : null; - return this.options.app.state.updateFilter({ - assigned: value, - assignees: null, - assigned_to_me: null - }); - } else { - return this.options.app.state.updateFilter({ - assigned: null, - assignees: this.getValue(), - assigned_to_me: null - }); - } - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const users = this.options.app.facets.users; - values.forEach(v => { - const login = v.val; - let name = ''; - if (login) { - const user = users.find(user => user.login === login); - if (user != null) { - name = user.name; - } - } - v.label = name; - }); - return values; - }, - - disable() { - return this.options.app.state.updateFilter({ - assigned: null, - assignees: null - }); - }, - - addCustomValue() { - const property = this.model.get('property'); - const customValue = this.$('.js-custom-value').select2('val'); - let value = this.getValue(); - if (value.length > 0) { - value += ','; - } - value += customValue; - const obj = {}; - obj[property] = value; - obj.assigned = null; - return this.options.app.state.updateFilter(obj); - }, - - sortValues(values) { - return sortBy(values, v => v.val === '' ? -999999 : -v.count); - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js deleted file mode 100644 index c652eba0abd..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export default CustomValuesFacet.extend({ - getUrl() { - return window.baseUrl + '/api/issues/authors'; - }, - - prepareSearch() { - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 2, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - formatInputTooShort() { - return translateWithParameters('select2.tooShort', 2); - }, - width: '100%', - ajax: { - quietMillis: 300, - url: this.getUrl(), - data(term) { - return { q: term, ps: 25 }; - }, - results(data) { - return { - more: false, - results: data.authors.map(author => { - return { id: author, text: author }; - }) - }; - } - } - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js deleted file mode 100644 index 5d05caaeb2e..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import BaseFacet from '../../../components/navigator/facets/base-facet'; -import Template from '../templates/facets/issues-base-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - onRender() { - BaseFacet.prototype.onRender.apply(this, arguments); - return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); - }, - - onDestroy() { - return this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - state: this.options.app.state.toJSON() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js deleted file mode 100644 index 81c31973b79..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import moment from 'moment'; -import { times } from 'lodash'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-creation-date-facet.hbs'; -import '../../../components/widgets/barchart'; -import { formatMeasure } from '../../../helpers/measures'; - -export default BaseFacet.extend({ - template: Template, - - events() { - return { - ...BaseFacet.prototype.events.apply(this, arguments), - 'change input': 'applyFacet', - 'click .js-select-period-start': 'selectPeriodStart', - 'click .js-select-period-end': 'selectPeriodEnd', - 'click .sonar-d3 rect': 'selectBar', - 'click .js-all': 'onAllClick', - 'click .js-last-week': 'onLastWeekClick', - 'click .js-last-month': 'onLastMonthClick', - 'click .js-last-year': 'onLastYearClick', - 'click .js-leak': 'onLeakClick' - }; - }, - - onRender() { - const that = this; - this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled')); - this.$('input').datepicker({ - dateFormat: 'yy-mm-dd', - changeMonth: true, - changeYear: true - }); - const props = ['createdAfter', 'createdBefore', 'createdAt']; - const query = this.options.app.state.get('query'); - props.forEach(prop => { - const value = query[prop]; - if (value != null) { - that.$(`input[name=${prop}]`).val(value); - } - }); - let values = this.model.getValues(); - if (!(Array.isArray(values) && values.length > 0)) { - let date = moment(); - values = []; - times(10, () => { - values.push({ count: 0, val: date.toDate().toString() }); - date = date.subtract(1, 'days'); - }); - values.reverse(); - } - values = values.map(v => { - const format = that.options.app.state.getFacetMode() === 'count' - ? 'SHORT_INT' - : 'SHORT_WORK_DUR'; - const text = formatMeasure(v.count, format); - return { ...v, text }; - }); - return this.$('.js-barchart').barchart(values); - }, - - selectPeriodStart() { - return this.$('.js-period-start').datepicker('show'); - }, - - selectPeriodEnd() { - return this.$('.js-period-end').datepicker('show'); - }, - - applyFacet() { - const obj = { createdAt: null, createdInLast: null }; - this.$('input').each(function() { - const property = $(this).prop('name'); - const value = $(this).val(); - obj[property] = value; - }); - return this.options.app.state.updateFilter(obj); - }, - - disable() { - return this.options.app.state.updateFilter({ - createdAfter: null, - createdBefore: null, - createdAt: null, - sinceLeakPeriod: null, - createdInLast: null - }); - }, - - selectBar(e) { - const periodStart = $(e.currentTarget).data('period-start'); - const periodEnd = $(e.currentTarget).data('period-end'); - return this.options.app.state.updateFilter({ - createdAfter: periodStart, - createdBefore: periodEnd, - createdAt: null, - sinceLeakPeriod: null, - createdInLast: null - }); - }, - - selectPeriod(period) { - return this.options.app.state.updateFilter({ - createdAfter: null, - createdBefore: null, - createdAt: null, - sinceLeakPeriod: null, - createdInLast: period - }); - }, - - onAllClick(e) { - e.preventDefault(); - return this.disable(); - }, - - onLastWeekClick(e) { - e.preventDefault(); - return this.selectPeriod('1w'); - }, - - onLastMonthClick(e) { - e.preventDefault(); - return this.selectPeriod('1m'); - }, - - onLastYearClick(e) { - e.preventDefault(); - return this.selectPeriod('1y'); - }, - - onLeakClick(e) { - e.preventDefault(); - this.options.app.state.updateFilter({ - createdAfter: null, - createdBefore: null, - createdAt: null, - createdInLast: null, - sinceLeakPeriod: 'true' - }); - }, - - serializeData() { - const hasLeak = this.options.app.state.get('contextComponentQualifier') === 'TRK'; - - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - hasLeak, - periodStart: this.options.app.state.get('query').createdAfter, - periodEnd: this.options.app.state.get('query').createdBefore, - createdAt: this.options.app.state.get('query').createdAt, - sinceLeakPeriod: this.options.app.state.get('query').sinceLeakPeriod, - createdInLast: this.options.app.state.get('query').createdInLast - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js deleted file mode 100644 index a6168f350f1..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-custom-values-facet.hbs'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export default BaseFacet.extend({ - template: Template, - - events() { - return { - ...BaseFacet.prototype.events.apply(this, arguments), - 'change .js-custom-value': 'addCustomValue' - }; - }, - - getUrl() {}, - - onRender() { - BaseFacet.prototype.onRender.apply(this, arguments); - return this.prepareSearch(); - }, - - prepareSearch() { - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 2, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - formatInputTooShort() { - return translateWithParameters('select2.tooShort', 2); - }, - width: '100%', - ajax: this.prepareAjaxSearch() - }); - }, - - prepareAjaxSearch() { - return { - quietMillis: 300, - url: this.getUrl(), - data(term, page) { - return { s: term, p: page }; - }, - results(data) { - return { more: data.more, results: data.results }; - } - }; - }, - - addCustomValue() { - const property = this.model.get('property'); - const customValue = this.$('.js-custom-value').select2('val'); - let value = this.getValue(); - if (value.length > 0) { - value += ','; - } - value += customValue; - const obj = {}; - obj[property] = value; - return this.options.app.state.updateFilter(obj); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js deleted file mode 100644 index 11b9a69a46d..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-file-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - onRender() { - BaseFacet.prototype.onRender.apply(this, arguments); - const widths = this.$('.facet-stat') - .map(function() { - return $(this).outerWidth(); - }) - .get(); - const maxValueWidth = Math.max(...widths); - return this.$('.facet-name').css('padding-right', maxValueWidth); - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const source = this.options.app.facets.components; - values.forEach(v => { - const key = v.val; - let label = null; - if (key) { - const item = source.find(file => file.uuid === key); - if (item != null) { - label = item.longName; - } - } - v.label = label; - }); - return values; - }, - - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js deleted file mode 100644 index d57d9c79cce..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-issue-key-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - onRender() { - return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues); - }, - - disable() { - return this.options.app.state.updateFilter({ issues: null }); - }, - - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - issues: this.options.app.state.get('query').issues - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js deleted file mode 100644 index 789ae16ca93..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export default CustomValuesFacet.extend({ - getUrl() { - return window.baseUrl + '/api/languages/list'; - }, - - prepareSearch() { - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 2, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - formatInputTooShort() { - return translateWithParameters('select2.tooShort', 2); - }, - width: '100%', - ajax: { - quietMillis: 300, - url: this.getUrl(), - data(term) { - return { q: term, ps: 0 }; - }, - results(data) { - return { - more: false, - results: data.languages.map(lang => { - return { id: lang.key, text: lang.name }; - }) - }; - } - } - }); - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const source = this.options.app.facets.languages; - values.forEach(v => { - const key = v.val; - let label = null; - if (key) { - const item = source.find(lang => lang.key === key); - if (item != null) { - label = item.name; - } - } - v.label = label; - }); - return values; - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js deleted file mode 100644 index dd54e82c8d2..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-mode-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - toggleFacet(e) { - const isCount = $(e.currentTarget).is('[data-value="count"]'); - return this.options.app.state.updateFilter({ - facetMode: isCount ? 'count' : 'effort' - }); - }, - - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - mode: this.options.app.state.getFacetMode() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js deleted file mode 100644 index b6e87598bff..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import BaseFacet from './base-facet'; - -export default BaseFacet.extend({ - getValuesWithLabels() { - const values = this.model.getValues(); - const components = this.options.app.facets.components; - values.forEach(v => { - const uuid = v.val; - let label = uuid; - if (uuid) { - const component = components.find(c => c.uuid === uuid); - if (component != null) { - label = component.longName; - } - } - v.label = label; - }); - return values; - }, - - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js deleted file mode 100644 index 9bd37e64b03..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; -import Template from '../templates/facets/issues-projects-facet.hbs'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { areThereCustomOrganizations, getOrganization } from '../../../store/organizations/utils'; - -export default CustomValuesFacet.extend({ - template: Template, - - getUrl() { - return window.baseUrl + '/api/components/search'; - }, - - prepareSearchForViews() { - const contextId = this.options.app.state.get('contextComponentUuid'); - return { - url: window.baseUrl + '/api/components/tree', - data(term, page) { - return { q: term, p: page, qualifiers: 'TRK', baseComponentId: contextId }; - } - }; - }, - - prepareAjaxSearch() { - const options = { - quietMillis: 300, - url: this.getUrl(), - data(term, page) { - return { q: term, p: page, qualifiers: 'TRK' }; - }, - results: r => ({ - more: r.paging.total > r.paging.pageIndex * r.paging.pageSize, - results: r.components.map(component => ({ - id: component.id, - text: component.name - })) - }) - }; - const contextQualifier = this.options.app.state.get('contextComponentQualifier'); - if (contextQualifier === 'VW' || contextQualifier === 'SVW') { - Object.assign(options, this.prepareSearchForViews()); - } - return options; - }, - - prepareSearch() { - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 3, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - formatInputTooShort() { - return translateWithParameters('select2.tooShort', 3); - }, - width: '100%', - ajax: this.prepareAjaxSearch() - }); - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const projects = this.options.app.facets.components; - const displayOrganizations = areThereCustomOrganizations(); - values.forEach(v => { - const uuid = v.val; - let label = ''; - let organization = null; - if (uuid) { - const project = projects.find(p => p.uuid === uuid); - if (project != null) { - label = project.longName; - organization = displayOrganizations && project.organization - ? getOrganization(project.organization) - : null; - } - } - v.label = label; - v.organization = organization; - }); - return values; - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js deleted file mode 100644 index 3e691aea9be..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; - -export default CustomValuesFacet.extend({ - getUrl() { - return window.baseUrl + '/api/users/search'; - }, - - prepareAjaxSearch() { - return { - quietMillis: 300, - url: this.getUrl(), - data(term, page) { - return { q: term, p: page }; - }, - results: window.usersToSelect2 - }; - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const source = this.options.app.facets.users; - values.forEach(v => { - const key = v.val; - let label = null; - if (key) { - const item = source.find(user => user.login === key); - if (item != null) { - label = item.name; - } - } - v.label = label; - }); - return values; - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js deleted file mode 100644 index d4e01b1c0f4..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import { sortBy } from 'lodash'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-resolution-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - onRender() { - BaseFacet.prototype.onRender.apply(this, arguments); - const value = this.options.app.state.get('query').resolved; - if (value != null && (!value || value === 'false')) { - this.$('.js-facet').filter('[data-unresolved]').addClass('active'); - } - }, - - toggleFacet(e) { - const unresolved = $(e.currentTarget).is('[data-unresolved]'); - $(e.currentTarget).toggleClass('active'); - if (unresolved) { - const checked = $(e.currentTarget).is('.active'); - const value = checked ? 'false' : null; - return this.options.app.state.updateFilter({ - resolved: value, - resolutions: null - }); - } else { - return this.options.app.state.updateFilter({ - resolved: null, - resolutions: this.getValue() - }); - } - }, - - disable() { - return this.options.app.state.updateFilter({ - resolved: null, - resolutions: null - }); - }, - - sortValues(values) { - const order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED']; - return sortBy(values, v => order.indexOf(v.val)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js deleted file mode 100644 index 75445570098..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export default CustomValuesFacet.extend({ - prepareSearch() { - let url = window.baseUrl + '/api/rules/search?f=name,langName'; - const languages = this.options.app.state.get('query').languages; - if (languages != null) { - url += '&languages=' + languages; - } - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 2, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - formatInputTooShort() { - return translateWithParameters('select2.tooShort', 2); - }, - width: '100%', - ajax: { - url, - quietMillis: 300, - data(term, page) { - return { q: term, p: page }; - }, - results(data) { - const results = data.rules.map(rule => { - const lang = rule.langName || translate('manual'); - return { - id: rule.key, - text: '(' + lang + ') ' + rule.name - }; - }); - return { - more: data.p * data.ps < data.total, - results - }; - } - } - }); - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - const rules = this.options.app.facets.rules; - values.forEach(v => { - const key = v.val; - let label = ''; - let extra = ''; - if (key) { - const rule = rules.find(r => r.key === key); - if (rule != null) { - label = rule.name; - } - if (rule != null) { - extra = rule.langName; - } - } - v.label = label; - v.extra = extra; - }); - return values; - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js deleted file mode 100644 index db5cd337838..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { sortBy } from 'lodash'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-severity-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - sortValues(values) { - const order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR']; - return sortBy(values, v => order.indexOf(v.val)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js deleted file mode 100644 index cb4e88a5f49..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { sortBy } from 'lodash'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-status-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - sortValues(values) { - const order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED']; - return sortBy(values, v => order.indexOf(v.val)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js deleted file mode 100644 index 8bb629ff3d4..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import CustomValuesFacet from './custom-values-facet'; -import { translate } from '../../../helpers/l10n'; - -export default CustomValuesFacet.extend({ - prepareSearch() { - let url = window.baseUrl + '/api/issues/tags?ps=10'; - const tags = this.options.app.state.get('query').tags; - if (tags != null) { - url += '&tags=' + tags; - } - return this.$('.js-custom-value').select2({ - placeholder: translate('search_verb'), - minimumInputLength: 0, - allowClear: false, - formatNoMatches() { - return translate('select2.noMatches'); - }, - formatSearching() { - return translate('select2.searching'); - }, - width: '100%', - ajax: { - url, - quietMillis: 300, - data(term) { - return { q: term, ps: 10 }; - }, - results(data) { - const results = data.tags.map(tag => { - return { id: tag, text: tag }; - }); - return { more: false, results }; - } - } - }); - }, - - getValuesWithLabels() { - const values = this.model.getValues(); - values.forEach(v => { - v.label = v.val; - v.extra = ''; - }); - return values; - }, - - serializeData() { - return { - ...CustomValuesFacet.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.getValuesWithLabels()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js deleted file mode 100644 index efac17ea9f4..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { sortBy } from 'lodash'; -import BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-type-facet.hbs'; - -export default BaseFacet.extend({ - template: Template, - - sortValues(values) { - const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; - return sortBy(values, v => order.indexOf(v.val)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/init.js b/server/sonar-web/src/main/js/apps/issues/init.js deleted file mode 100644 index 0fb32e839ab..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/init.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import Backbone from 'backbone'; -import Marionette from 'backbone.marionette'; -import State from './models/state'; -import Layout from './layout'; -import Issues from './models/issues'; -import Facets from '../../components/navigator/models/facets'; -import Controller from './controller'; -import Router from './router'; -import WorkspaceListView from './workspace-list-view'; -import WorkspaceHeaderView from './workspace-header-view'; -import FacetsView from './facets-view'; -import HeaderView from './HeaderView'; - -const App = new Marionette.Application(); -const init = function({ el, user }) { - this.state = new State({ user, canBulkChange: user.isLoggedIn }); - this.list = new Issues(); - this.facets = new Facets(); - - this.layout = new Layout({ app: this, el }); - this.layout.render(); - $('#footer').addClass('search-navigator-footer'); - - this.controller = new Controller({ app: this }); - - this.issuesView = new WorkspaceListView({ - app: this, - collection: this.list - }); - this.layout.workspaceListRegion.show(this.issuesView); - this.issuesView.bindScrollEvents(); - - this.workspaceHeaderView = new WorkspaceHeaderView({ - app: this, - collection: this.list - }); - this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView); - - this.facetsView = new FacetsView({ - app: this, - collection: this.facets - }); - this.layout.facetsRegion.show(this.facetsView); - - this.headerView = new HeaderView({ - app: this - }); - this.layout.filtersRegion.show(this.headerView); - - key.setScope('list'); - App.router = new Router({ app: App }); - Backbone.history.start(); -}; - -App.on('start', el => { - init.call(App, el); -}); - -export default function(el, user) { - App.start({ el, user }); - - return () => { - Backbone.history.stop(); - App.layout.destroy(); - $('#footer').removeClass('search-navigator-footer'); - }; -} diff --git a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js b/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js deleted file mode 100644 index c3f45efbfe9..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import ActionOptionsView from '../../components/common/action-options-view'; -import Template from './templates/issues-issue-filter-form.hbs'; - -export default ActionOptionsView.extend({ - template: Template, - - selectOption(e) { - const property = $(e.currentTarget).data('property'); - const value = $(e.currentTarget).data('value'); - this.trigger('select', property, value); - ActionOptionsView.prototype.selectOption.apply(this, arguments); - }, - - serializeData() { - return { - ...ActionOptionsView.prototype.serializeData.apply(this, arguments), - s: this.model.get('severity') - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/layout.js b/server/sonar-web/src/main/js/apps/issues/layout.js deleted file mode 100644 index c796baccb9c..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/layout.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import Marionette from 'backbone.marionette'; -import Template from './templates/issues-layout.hbs'; -import './styles.css'; - -export default Marionette.LayoutView.extend({ - template: Template, - - regions: { - filtersRegion: '.issues-header', - facetsRegion: '.search-navigator-facets', - workspaceHeaderRegion: '.search-navigator-workspace-header', - workspaceListRegion: '.search-navigator-workspace-list', - workspaceComponentViewerRegion: '.issues-workspace-component-viewer' - }, - - onRender() { - this.$('.search-navigator').addClass('sticky'); - const top = this.$('.search-navigator').offset().top; - this.$('.search-navigator-workspace-header').css({ top }); - this.$('.search-navigator-side').css({ top }).isolatedScroll(); - }, - - showSpinner(region) { - return this[region].show( - new Marionette.ItemView({ - template: () => '<i class="spinner"></i>' - }) - ); - }, - - showComponentViewer() { - this.scroll = $(window).scrollTop(); - this.$('.issues').addClass('issues-extended-view'); - }, - - hideComponentViewer() { - this.$('.issues').removeClass('issues-extended-view'); - if (this.scroll != null) { - $(window).scrollTop(this.scroll); - } - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/models/issues.js b/server/sonar-web/src/main/js/apps/issues/models/issues.js deleted file mode 100644 index 9ea6454a484..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/models/issues.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Backbone from 'backbone'; -import Issue from './issue'; - -export default Backbone.Collection.extend({ - model: Issue, - - url() { - return window.baseUrl + '/api/issues/search'; - }, - - _injectRelational(issue, source, baseField, lookupField) { - const baseValue = issue[baseField]; - if (baseValue != null && Array.isArray(source) && source.length > 0) { - const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); - if (lookupValue != null) { - Object.keys(lookupValue).forEach(key => { - const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1); - issue[newKey] = lookupValue[key]; - }); - } - } - return issue; - }, - - _injectCommentsRelational(issue, users) { - if (issue.comments) { - const that = this; - const newComments = issue.comments.map(comment => { - let newComment = { ...comment, author: comment.login }; - delete newComment.login; - newComment = that._injectRelational(newComment, users, 'author', 'login'); - return newComment; - }); - issue = { ...issue, comments: newComments }; - } - return issue; - }, - - _prepareClosed(issue) { - if (issue.status === 'CLOSED') { - issue.flows = []; - delete issue.textRange; - } - return issue; - }, - - ensureTextRange(issue) { - if (issue.line && !issue.textRange) { - // FIXME 999999 - issue.textRange = { - startLine: issue.line, - endLine: issue.line, - startOffset: 0, - endOffset: 999999 - }; - } - return issue; - }, - - parseIssues(r, startIndex = 0) { - const that = this; - return r.issues.map((issue, index) => { - Object.assign(issue, { index: startIndex + index }); - issue = that._injectRelational(issue, r.components, 'component', 'key'); - issue = that._injectRelational(issue, r.components, 'project', 'key'); - issue = that._injectRelational(issue, r.components, 'subProject', 'key'); - issue = that._injectRelational(issue, r.rules, 'rule', 'key'); - issue = that._injectRelational(issue, r.users, 'assignee', 'login'); - issue = that._injectCommentsRelational(issue, r.users); - issue = that._prepareClosed(issue); - issue = that.ensureTextRange(issue); - return issue; - }); - }, - - setIndex() { - return this.forEach((issue, index) => issue.set({ index })); - }, - - selectByKeys(keys) { - const that = this; - keys.forEach(key => { - const issue = that.get(key); - if (issue) { - issue.set({ selected: true }); - } - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/models/state.js b/server/sonar-web/src/main/js/apps/issues/models/state.js deleted file mode 100644 index b367cc670b0..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/models/state.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import State from '../../../components/navigator/models/state'; - -export default State.extend({ - defaults: { - page: 1, - maxResultsReached: false, - query: {}, - facets: ['facetMode', 'types', 'resolutions'], - isContext: false, - allFacets: [ - 'facetMode', - 'issues', - 'types', - 'resolutions', - 'severities', - 'statuses', - 'createdAt', - 'rules', - 'tags', - 'projectUuids', - 'moduleUuids', - 'directories', - 'fileUuids', - 'assignees', - 'authors', - 'languages' - ], - facetsFromServer: [ - 'types', - 'severities', - 'statuses', - 'resolutions', - 'projectUuids', - 'directories', - 'rules', - 'moduleUuids', - 'tags', - 'assignees', - 'authors', - 'fileUuids', - 'languages', - 'createdAt' - ], - transform: { - resolved: 'resolutions', - assigned: 'assignees', - createdBefore: 'createdAt', - createdAfter: 'createdAt', - sinceLeakPeriod: 'createdAt', - createdInLast: 'createdAt' - } - }, - - getFacetMode() { - const query = this.get('query'); - return query.facetMode || 'count'; - }, - - toJSON() { - return { facetMode: this.getFacetMode(), ...this.attributes }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/redirects.js b/server/sonar-web/src/main/js/apps/issues/redirects.js new file mode 100644 index 00000000000..3efc1af64be --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/redirects.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils'; +import type { RawQuery } from './utils'; + +const parseHash = (hash: string): RawQuery => { + const query: RawQuery = {}; + const parts = hash.split('|'); + parts.forEach(part => { + const tokens = part.split('='); + if (tokens.length === 2) { + const property = decodeURIComponent(tokens[0]); + const value = decodeURIComponent(tokens[1]); + if (property === 'assigned_to_me' && value === 'true') { + query.myIssues = 'true'; + } else { + query[property] = value; + } + } + }); + return query; +}; + +export const onEnter = (state: Object, replace: Function) => { + const { hash } = window.location; + if (hash.length > 1) { + const query = parseHash(hash.substr(1)); + const normalizedQuery = { + ...serializeQuery(parseQuery(query)), + myIssues: areMyIssuesSelected(query) ? 'true' : undefined + }; + replace({ + pathname: state.location.pathname, + query: normalizedQuery + }); + } +}; diff --git a/server/sonar-web/src/main/js/apps/issues/router.js b/server/sonar-web/src/main/js/apps/issues/router.js deleted file mode 100644 index ac35322a355..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/router.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import Router from '../../components/navigator/router'; - -export default Router.extend({ - routes: { - '': 'home', - ':query': 'index' - }, - - home() { - return this.navigate('resolved=false', { trigger: true, replace: true }); - }, - - index(query) { - this.options.app.state.setQuery(this.options.app.controller.parseQuery(query)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/routes.js b/server/sonar-web/src/main/js/apps/issues/routes.js index 5f3a27c905c..61893ce7fec 100644 --- a/server/sonar-web/src/main/js/apps/issues/routes.js +++ b/server/sonar-web/src/main/js/apps/issues/routes.js @@ -17,13 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { onEnter } from './redirects'; + const routes = [ { - indexRoute: { - getComponent(_, callback) { - require.ensure([], require => - callback(null, require('./components/IssuesAppContainer').default)); - } + getIndexRoute(_, callback) { + require.ensure([], require => + callback(null, { + component: require('./components/AppContainer').default, + onEnter + })); } } ]; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js new file mode 100644 index 00000000000..cf7bb4be8f2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js @@ -0,0 +1,168 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, uniq, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import FacetFooter from './components/FacetFooter'; +import { searchAssignees } from '../utils'; +import type { ReferencedUser, Component } from '../utils'; +import Avatar from '../../../components/ui/Avatar'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + assigned: boolean, + assignees: Array<string>, + component?: Component, + facetMode: string, + onChange: (changes: {}) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedUsers: { [string]: ReferencedUser } +|}; + +export default class AssigneeFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'assignees'; + + handleItemClick = (itemValue: string) => { + if (itemValue === '') { + // unassigned + this.props.onChange({ assigned: !this.props.assigned, assignees: [] }); + } else { + // defined assignee + const { assignees } = this.props; + const newValue = sortBy( + assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue] + ); + this.props.onChange({ assigned: true, assignees: newValue }); + } + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + handleSearch = (query: string) => searchAssignees(query, this.props.component); + + handleSelect = (assignee: string) => { + const { assignees } = this.props; + this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) }); + }; + + isAssigneeActive(assignee: string) { + return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee); + } + + getAssigneeName(assignee: string): React.Element<*> | string { + if (assignee === '') { + return translate('unassigned'); + } else { + const { referencedUsers } = this.props; + if (referencedUsers[assignee]) { + return ( + <span> + <Avatar + className="little-spacer-right" + hash={referencedUsers[assignee].avatar} + size={16} + /> + {referencedUsers[assignee].name} + </span> + ); + } else { + return assignee; + } + } + } + + getStat(assignee: string): ?number { + const { stats } = this.props; + return stats ? stats[assignee] : null; + } + + renderOption = (option: { avatar: string, label: string }) => { + return ( + <span> + {option.avatar != null && + <Avatar className="little-spacer-right" hash={option.avatar} size={16} />} + {option.label} + </span> + ); + }; + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const assignees = sortBy( + Object.keys(stats), + // put unassigned first + key => key === '' ? 0 : 1, + // the sort by number + key => -stats[key] + ); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={!this.props.assigned || this.props.assignees.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {assignees.map(assignee => ( + <FacetItem + active={this.isAssigneeActive(assignee)} + facetMode={this.props.facetMode} + key={assignee} + name={this.getAssigneeName(assignee)} + onClick={this.handleItemClick} + stat={this.getStat(assignee)} + value={assignee} + /> + ))} + </FacetItemsList>} + + {this.props.open && + <FacetFooter + onSearch={this.handleSearch} + onSelect={this.handleSelect} + renderOption={this.renderOption} + />} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js new file mode 100644 index 00000000000..d539229ff78 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: {}) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + authors: Array<string> +|}; + +export default class AuthorFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'authors'; + + handleItemClick = (itemValue: string) => { + const { authors } = this.props; + const newValue = sortBy( + authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(author: string): ?number { + const { stats } = this.props; + return stats ? stats[author] : null; + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const authors = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.authors.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {authors.map(author => ( + <FacetItem + active={this.props.authors.includes(author)} + facetMode={this.props.facetMode} + key={author} + name={author} + onClick={this.handleItemClick} + stat={this.getStat(author)} + value={author} + /> + ))} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js new file mode 100644 index 00000000000..dd28ccfddea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js @@ -0,0 +1,276 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import moment from 'moment'; +import { max } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import { BarChart } from '../../../components/charts/bar-chart'; +import DateInput from '../../../components/controls/DateInput'; +import { translate } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; +import type { Component } from '../utils'; + +type Props = {| + component?: Component, + createdAfter: string, + createdAt: string, + createdBefore: string, + createdInLast: string, + facetMode: string, + onChange: (changes: {}) => void, + onToggle: (property: string) => void, + open: boolean, + sinceLeakPeriod: boolean, + stats?: { [string]: number } +|}; + +const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; + +export default class CreationDateFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'createdAt'; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + resetTo = (changes: {}) => { + this.props.onChange({ + createdAfter: undefined, + createdAt: undefined, + createdBefore: undefined, + createdInLast: undefined, + sinceLeakPeriod: undefined, + ...changes + }); + }; + + handleBarClick = ( + { createdAfter, createdBefore }: { createdAfter: Object, createdBefore?: Object } + ) => { + this.resetTo({ + createdAfter: createdAfter.format(DATE_FORMAT), + createdBefore: createdBefore && createdBefore.format(DATE_FORMAT) + }); + }; + + handlePeriodChange = (property: string) => + (value: string) => { + this.props.onChange({ + createdAt: undefined, + createdInLast: undefined, + sinceLeakPeriod: undefined, + [property]: value + }); + }; + + handlePeriodClick = (period?: string) => + (e: Event & { target: HTMLElement }) => { + e.preventDefault(); + e.target.blur; + this.resetTo({ createdInLast: period }); + }; + + handleLeakPeriodClick = () => + (e: Event & { target: HTMLElement }) => { + e.preventDefault(); + e.target.blur; + this.resetTo({ sinceLeakPeriod: true }); + }; + + renderBarChart() { + const { createdBefore, stats } = this.props; + + if (!stats) { + return null; + } + + const periods = Object.keys(stats); + + if (periods.length < 2) { + return null; + } + + const data = periods.map((startDate, index) => { + const startMoment = moment(startDate); + const nextStartMoment = index < periods.length - 1 + ? moment(periods[index + 1]) + : createdBefore ? moment(createdBefore) : undefined; + const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days'); + + let tooltip = formatMeasure(stats[startDate], 'SHORT_INT') + + '<br>' + + startMoment.format('LL'); + + if (endMoment) { + const isSameDay = endMoment.diff(startMoment, 'days') <= 1; + if (!isSameDay) { + tooltip += ' – ' + endMoment.format('LL'); + } + } + + return { + createdAfter: startMoment, + createdBefore: nextStartMoment, + startMoment, + tooltip, + x: index, + y: stats[startDate] + }; + }); + + const barsWidth = Math.floor(240 / data.length); + const width = barsWidth * data.length - 1 + 20; + + const maxValue = max(data.map(d => d.y)); + const format = this.props.facetMode === 'count' ? 'SHORT_INT' : 'SHORT_WORK_DUR'; + const xValues = data.map(d => d.y === maxValue ? formatMeasure(maxValue, format) : ''); + + return ( + <BarChart + barsWidth={barsWidth - 1} + data={data} + height={75} + onBarClick={this.handleBarClick} + padding={[25, 10, 5, 10]} + width={width} + xValues={xValues} + /> + ); + } + + renderExactDate() { + const m = moment(this.props.createdAt); + return ( + <div className="search-navigator-facet-container"> + {m.format('LLL')} + <br /> + <span className="note">({m.fromNow()})</span> + </div> + ); + } + + renderPeriodSelectors() { + const { createdAfter, createdBefore } = this.props; + + return ( + <div className="search-navigator-date-facet-selection"> + <DateInput + className="search-navigator-date-facet-selection-dropdown-left" + onChange={this.handlePeriodChange('createdAfter')} + placeholder={translate('from')} + value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined} + /> + <DateInput + className="search-navigator-date-facet-selection-dropdown-right" + onChange={this.handlePeriodChange('createdBefore')} + placeholder={translate('to')} + value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined} + /> + </div> + ); + } + + renderPrefefinedPeriods() { + const { component, createdInLast, sinceLeakPeriod } = this.props; + return ( + <div className="spacer-top"> + <span className="spacer-right">{translate('issues.facet.createdAt.or')}</span> + <a className="spacer-right" href="#" onClick={this.handlePeriodClick()}> + {translate('issues.facet.createdAt.all')} + </a> + {component == null && + <a + className={classNames('spacer-right', { 'active-link': createdInLast === '1w' })} + href="#" + onClick={this.handlePeriodClick('1w')}> + {translate('issues.facet.createdAt.last_week')} + </a>} + {component == null && + <a + className={classNames('spacer-right', { 'active-link': createdInLast === '1m' })} + href="#" + onClick={this.handlePeriodClick('1m')}> + {translate('issues.facet.createdAt.last_month')} + </a>} + {component == null && + <a + className={classNames('spacer-right', { 'active-link': createdInLast === '1y' })} + href="#" + onClick={this.handlePeriodClick('1y')}> + {translate('issues.facet.createdAt.last_year')} + </a>} + {component != null && + <a + className={classNames('spacer-right', { 'active-link': sinceLeakPeriod })} + href="#" + onClick={this.handleLeakPeriodClick()}> + {translate('issues.leak_period')} + </a>} + </div> + ); + } + + renderInner() { + const { createdAt } = this.props; + return createdAt + ? this.renderExactDate() + : <div> + {this.renderBarChart()} + {this.renderPeriodSelectors()} + {this.renderPrefefinedPeriods()} + </div>; + } + + render() { + const hasValue = this.props.createdAfter.length > 0 || + this.props.createdAt.length > 0 || + this.props.createdBefore.length > 0 || + this.props.createdInLast.length > 0 || + this.props.sinceLeakPeriod; + + const { stats } = this.props; + + if (!stats) { + return null; + } + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={hasValue} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && this.renderInner()} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js new file mode 100644 index 00000000000..ddd82db1eab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import type { ReferencedComponent } from '../utils'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedComponents: { [string]: ReferencedComponent }, + directories: Array<string> +|}; + +export default class DirectoryFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'directories'; + + handleItemClick = (itemValue: string) => { + const { directories } = this.props; + const newValue = sortBy( + directories.includes(itemValue) + ? without(directories, itemValue) + : [...directories, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(directory: string): ?number { + const { stats } = this.props; + return stats ? stats[directory] : null; + } + + renderName(directory: string): React.Element<*> | string { + // `referencedComponents` are indexed by uuid + // so we have to browse them all to find a matching one + const { referencedComponents } = this.props; + const uuid = Object.keys(referencedComponents).find( + uuid => referencedComponents[uuid].key === directory + ); + const name = uuid ? referencedComponents[uuid].name : directory; + return ( + <span> + <QualifierIcon className="little-spacer-right" qualifier="DIR" /> + {name} + </span> + ); + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const directories = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.directories.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {directories.map(directory => ( + <FacetItem + active={this.props.directories.includes(directory)} + facetMode={this.props.facetMode} + key={directory} + name={this.renderName(directory)} + onClick={this.handleItemClick} + stat={this.getStat(directory)} + value={directory} + /> + ))} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js new file mode 100644 index 00000000000..dcfb16202e9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: {}) => void +|}; + +export default class FacetMode extends React.PureComponent { + props: Props; + + property = 'facetMode'; + + handleItemClick = (itemValue: string) => { + this.props.onChange({ [this.property]: itemValue }); + }; + + render() { + const { facetMode } = this.props; + const modes = ['count', 'effort']; + + return ( + <FacetBox property={this.property}> + <FacetHeader name={translate('issues.facet.mode')} /> + + <FacetItemsList> + {modes.map(mode => ( + <FacetItem + active={facetMode === mode} + facetMode={this.props.facetMode} + halfWidth={true} + key={mode} + name={translate('issues.facet.mode', mode)} + onClick={this.handleItemClick} + stat={null} + value={mode} + /> + ))} + </FacetItemsList> + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js new file mode 100644 index 00000000000..5d914f8380d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import type { ReferencedComponent } from '../utils'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; +import { collapsePath } from '../../../helpers/path'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedComponents: { [string]: ReferencedComponent }, + files: Array<string> +|}; + +export default class FileFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'files'; + + handleItemClick = (itemValue: string) => { + const { files } = this.props; + const newValue = sortBy( + files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(file: string): ?number { + const { stats } = this.props; + return stats ? stats[file] : null; + } + + renderName(file: string): React.Element<*> | string { + const { referencedComponents } = this.props; + const name = referencedComponents[file] + ? collapsePath(referencedComponents[file].path, 15) + : file; + return ( + <span> + <QualifierIcon className="little-spacer-right" qualifier="FIL" /> + {name} + </span> + ); + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const files = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.files.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {files.map(file => ( + <FacetItem + active={this.props.files.includes(file)} + facetMode={this.props.facetMode} + key={file} + name={this.renderName(file)} + onClick={this.handleItemClick} + stat={this.getStat(file)} + value={file} + /> + ))} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js new file mode 100644 index 00000000000..411c9b74d2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js @@ -0,0 +1,114 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, uniq, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import LanguageFacetFooter from './LanguageFacetFooter'; +import type { ReferencedLanguage } from '../utils'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedLanguages: { [string]: ReferencedLanguage }, + languages: Array<string> +|}; + +export default class LanguageFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'languages'; + + handleItemClick = (itemValue: string) => { + const { languages } = this.props; + const newValue = sortBy( + languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getLanguageName(language: string): string { + const { referencedLanguages } = this.props; + return referencedLanguages[language] ? referencedLanguages[language].name : language; + } + + getStat(language: string): ?number { + const { stats } = this.props; + return stats ? stats[language] : null; + } + + handleSelect = (language: string) => { + const { languages } = this.props; + this.props.onChange({ [this.property]: uniq([...languages, language]) }); + }; + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const languages = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.languages.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {languages.map(language => ( + <FacetItem + active={this.props.languages.includes(language)} + facetMode={this.props.facetMode} + key={language} + name={this.getLanguageName(language)} + onClick={this.handleItemClick} + stat={this.getStat(language)} + value={language} + /> + ))} + </FacetItemsList>} + + {this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js new file mode 100644 index 00000000000..4ad3bc32825 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Select from 'react-select'; +import { connect } from 'react-redux'; +import { translate } from '../../../helpers/l10n'; +import { getLanguages } from '../../../store/rootReducer'; + +type Option = { label: string, value: string }; + +type Props = {| + languages: Array<{ key: string, name: string }>, + onSelect: (value: string) => void +|}; + +class LanguageFacetFooter extends React.PureComponent { + props: Props; + + handleChange = (option: Option) => { + this.props.onSelect(option.value); + }; + + render() { + const options = this.props.languages.map(language => ({ + label: language.name, + value: language.key + })); + + return ( + <div className="search-navigator-facet-footer"> + <Select + autofocus={true} + className="input-super-large" + clearable={false} + noResultsText={translate('select2.noMatches')} + onChange={this.handleChange} + options={options} + placeholder={translate('search_verb')} + searchable={true} + /> + </div> + ); + } +} + +const mapStateToProps = state => ({ + languages: Object.values(getLanguages(state)) +}); + +export default connect(mapStateToProps)(LanguageFacetFooter); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js new file mode 100644 index 00000000000..8711e017462 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import type { ReferencedComponent } from '../utils'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedComponents: { [string]: ReferencedComponent }, + modules: Array<string> +|}; + +export default class ModuleFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'modules'; + + handleItemClick = (itemValue: string) => { + const { modules } = this.props; + const newValue = sortBy( + modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(module: string): ?number { + const { stats } = this.props; + return stats ? stats[module] : null; + } + + renderName(module: string): React.Element<*> | string { + const { referencedComponents } = this.props; + const name = referencedComponents[module] ? referencedComponents[module].name : module; + return ( + <span> + <QualifierIcon className="little-spacer-right" qualifier="BRC" /> + {name} + </span> + ); + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const modules = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.modules.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {modules.map(module => ( + <FacetItem + active={this.props.modules.includes(module)} + facetMode={this.props.facetMode} + key={module} + name={this.renderName(module)} + onClick={this.handleItemClick} + stat={this.getStat(module)} + value={module} + /> + ))} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js new file mode 100644 index 00000000000..2b7046f69ef --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js @@ -0,0 +1,160 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, uniq, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import FacetFooter from './components/FacetFooter'; +import type { ReferencedComponent, Component } from '../utils'; +import Organization from '../../../components/shared/Organization'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import { searchComponents, getTree } from '../../../api/components'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + component?: Component, + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedComponents: { [string]: ReferencedComponent }, + projects: Array<string> +|}; + +export default class ProjectFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'projects'; + + handleItemClick = (itemValue: string) => { + const { projects } = this.props; + const newValue = sortBy( + projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + handleSearch = (query: string) => { + const { component } = this.props; + + return component != null && ['VW', 'SVW'].includes(component.qualifier) + ? getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response => + response.components.map(component => ({ + label: component.name, + organization: component.organization, + value: component.refId + }))) + : searchComponents({ ps: 50, q: query, qualifiers: 'TRK' }).then(response => + response.components.map(component => ({ + label: component.name, + organization: component.organization, + value: component.id + }))); + }; + + handleSelect = (rule: string) => { + const { projects } = this.props; + this.props.onChange({ [this.property]: uniq([...projects, rule]) }); + }; + + getStat(project: string): ?number { + const { stats } = this.props; + return stats ? stats[project] : null; + } + + renderName(project: string): React.Element<*> | string { + const { referencedComponents } = this.props; + return referencedComponents[project] + ? <span> + <QualifierIcon className="little-spacer-right" qualifier="TRK" /> + <Organization link={false} organizationKey={referencedComponents[project].organization} /> + {referencedComponents[project].name} + </span> + : <span> + <QualifierIcon className="little-spacer-right" qualifier="TRK" /> + {project} + </span>; + } + + renderOption = (option: { label: string, organization: string }) => { + return ( + <span> + <Organization link={false} organizationKey={option.organization} /> + {option.label} + </span> + ); + }; + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const projects = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.projects.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {projects.map(project => ( + <FacetItem + active={this.props.projects.includes(project)} + facetMode={this.props.facetMode} + key={project} + name={this.renderName(project)} + onClick={this.handleItemClick} + stat={this.getStat(project)} + value={project} + /> + ))} + </FacetItemsList>} + + {this.props.open && + <FacetFooter + minimumQueryLength={3} + onSearch={this.handleSearch} + onSelect={this.handleSelect} + renderOption={this.renderOption} + />} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js new file mode 100644 index 00000000000..d83c56cd917 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { orderBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: {}) => void, + onToggle: (property: string) => void, + open: boolean, + resolved: boolean, + resolutions: Array<string>, + stats?: { [string]: number } +|}; + +export default class ResolutionFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'resolutions'; + + handleItemClick = (itemValue: string) => { + if (itemValue === '') { + // unresolved + this.props.onChange({ resolved: !this.props.resolved, resolutions: [] }); + } else { + // defined resolution + const { resolutions } = this.props; + const newValue = orderBy( + resolutions.includes(itemValue) + ? without(resolutions, itemValue) + : [...resolutions, itemValue] + ); + this.props.onChange({ resolved: true, resolutions: newValue }); + } + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + isFacetItemActive(resolution: string) { + return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution); + } + + getFacetItemName(resolution: string) { + return resolution === '' ? translate('unresolved') : translate('issue.resolution', resolution); + } + + getStat(resolution: string): ?number { + const { stats } = this.props; + return stats ? stats[resolution] : null; + } + + renderItem = (resolution: string) => { + const active = this.isFacetItemActive(resolution); + const stat = this.getStat(resolution); + + return ( + <FacetItem + active={active} + disabled={stat === 0 && !active} + facetMode={this.props.facetMode} + key={resolution} + halfWidth={true} + name={this.getFacetItemName(resolution)} + onClick={this.handleItemClick} + stat={stat} + value={resolution} + /> + ); + }; + + render() { + const resolutions = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED']; + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={!this.props.resolved || this.props.resolutions.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {resolutions.map(this.renderItem)} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js new file mode 100644 index 00000000000..ce5d5d49468 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js @@ -0,0 +1,126 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, uniq, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import FacetFooter from './components/FacetFooter'; +import { searchRules } from '../../../api/rules'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + languages: Array<string>, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + referencedRules: { [string]: { name: string } }, + rules: Array<string> +|}; + +export default class RuleFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'rules'; + + handleItemClick = (itemValue: string) => { + const { rules } = this.props; + const newValue = sortBy( + rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + handleSearch = (query: string) => { + const { languages } = this.props; + return searchRules({ + f: 'name,langName', + languages: languages.length ? languages.join() : undefined, + q: query + }).then(response => + response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key }))); + }; + + handleSelect = (rule: string) => { + const { rules } = this.props; + this.props.onChange({ [this.property]: uniq([...rules, rule]) }); + }; + + getRuleName(rule: string): string { + const { referencedRules } = this.props; + return referencedRules[rule] ? referencedRules[rule].name : rule; + } + + getStat(rule: string): ?number { + const { stats } = this.props; + return stats ? stats[rule] : null; + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const rules = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.rules.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {rules.map(rule => ( + <FacetItem + active={this.props.rules.includes(rule)} + facetMode={this.props.facetMode} + key={rule} + name={this.getRuleName(rule)} + onClick={this.handleItemClick} + stat={this.getStat(rule)} + value={rule} + /> + ))} + </FacetItemsList>} + + {this.props.open && + <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js index cadec2974e3..e95f44008d0 100644 --- a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import { orderBy, uniq, without } from 'lodash'; +import { orderBy, without } from 'lodash'; import FacetBox from './components/FacetBox'; import FacetHeader from './components/FacetHeader'; import FacetItem from './components/FacetItem'; @@ -28,6 +28,7 @@ import SeverityHelper from '../../../components/shared/SeverityHelper'; import { translate } from '../../../helpers/l10n'; type Props = {| + facetMode: string, onChange: (changes: { [string]: Array<string> }) => void, onToggle: (property: string) => void, open: boolean, @@ -47,9 +48,7 @@ export default class SeverityFacet extends React.PureComponent { handleItemClick = (itemValue: string) => { const { severities } = this.props; const newValue = orderBy( - severities.includes(itemValue) - ? without(severities, itemValue) - : uniq([...severities, itemValue]) + severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue] ); this.props.onChange({ [this.property]: newValue }); }; @@ -63,6 +62,25 @@ export default class SeverityFacet extends React.PureComponent { return stats ? stats[severity] : null; } + renderItem = (severity: string) => { + const active = this.props.severities.includes(severity); + const stat = this.getStat(severity); + + return ( + <FacetItem + active={active} + disabled={stat === 0 && !active} + facetMode={this.props.facetMode} + halfWidth={true} + key={severity} + name={<SeverityHelper severity={severity} />} + onClick={this.handleItemClick} + stat={stat} + value={severity} + /> + ); + }; + render() { const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR']; @@ -75,19 +93,10 @@ export default class SeverityFacet extends React.PureComponent { open={this.props.open} /> - <FacetItemsList open={this.props.open}> - {severities.map(severity => ( - <FacetItem - active={this.props.severities.includes(severity)} - halfWidth={true} - key={severity} - name={<SeverityHelper severity={severity} />} - onClick={this.handleItemClick} - stat={this.getStat(severity)} - value={severity} - /> - ))} - </FacetItemsList> + {this.props.open && + <FacetItemsList> + {severities.map(this.renderItem)} + </FacetItemsList>} </FacetBox> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js new file mode 100644 index 00000000000..fd4c2c3f91c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js @@ -0,0 +1,212 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import AssigneeFacet from './AssigneeFacet'; +import AuthorFacet from './AuthorFacet'; +import CreationDateFacet from './CreationDateFacet'; +import DirectoryFacet from './DirectoryFacet'; +import FacetMode from './FacetMode'; +import FileFacet from './FileFacet'; +import LanguageFacet from './LanguageFacet'; +import ModuleFacet from './ModuleFacet'; +import ProjectFacet from './ProjectFacet'; +import ResolutionFacet from './ResolutionFacet'; +import RuleFacet from './RuleFacet'; +import SeverityFacet from './SeverityFacet'; +import StatusFacet from './StatusFacet'; +import TagFacet from './TagFacet'; +import TypeFacet from './TypeFacet'; +import type { + Query, + Facet, + ReferencedComponent, + ReferencedUser, + ReferencedLanguage, + Component +} from '../utils'; + +type Props = {| + component?: Component, + facets: { [string]: Facet }, + myIssues: boolean, + onFacetToggle: (property: string) => void, + onFilterChange: (changes: { [string]: Array<string> }) => void, + openFacets: { [string]: boolean }, + query: Query, + referencedComponents: { [string]: ReferencedComponent }, + referencedLanguages: { [string]: ReferencedLanguage }, + referencedRules: { [string]: { name: string } }, + referencedUsers: { [string]: ReferencedUser } +|}; + +export default class Sidebar extends React.PureComponent { + props: Props; + + render() { + const { component, facets, openFacets, query } = this.props; + + const displayProjectsFacet: boolean = component == null || + !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier); + const displayModulesFacet = component == null || component.qualifier !== 'DIR'; + const displayDirectoriesFacet = component == null || component.qualifier !== 'DIR'; + const displayAuthorFacet = component == null || component.qualifier !== 'DEV'; + + return ( + <div className="search-navigator-facets-list"> + <FacetMode facetMode={query.facetMode} onChange={this.props.onFilterChange} /> + <TypeFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.types} + stats={facets.types} + types={query.types} + /> + <ResolutionFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.resolutions} + resolved={query.resolved} + resolutions={query.resolutions} + stats={facets.resolutions} + /> + <SeverityFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.severities} + severities={query.severities} + stats={facets.severities} + /> + <StatusFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.statuses} + stats={facets.statuses} + statuses={query.statuses} + /> + <CreationDateFacet + component={component} + createdAfter={query.createdAfter} + createdAt={query.createdAt} + createdBefore={query.createdBefore} + createdInLast={query.createdInLast} + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.createdAt} + sinceLeakPeriod={query.sinceLeakPeriod} + stats={facets.createdAt} + /> + <RuleFacet + facetMode={query.facetMode} + languages={query.languages} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.rules} + stats={facets.rules} + referencedRules={this.props.referencedRules} + rules={query.rules} + /> + <TagFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.tags} + stats={facets.tags} + tags={query.tags} + /> + {displayProjectsFacet && + <ProjectFacet + component={component} + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.projects} + projects={query.projects} + referencedComponents={this.props.referencedComponents} + stats={facets.projects} + />} + {displayModulesFacet && + <ModuleFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.modules} + modules={query.modules} + referencedComponents={this.props.referencedComponents} + stats={facets.modules} + />} + {displayDirectoriesFacet && + <DirectoryFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.directories} + directories={query.directories} + referencedComponents={this.props.referencedComponents} + stats={facets.directories} + />} + <FileFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.files} + files={query.files} + referencedComponents={this.props.referencedComponents} + stats={facets.files} + /> + {!this.props.myIssues && + <AssigneeFacet + component={component} + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.assignees} + assigned={query.assigned} + assignees={query.assignees} + referencedUsers={this.props.referencedUsers} + stats={facets.assignees} + />} + {displayAuthorFacet && + <AuthorFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.authors} + authors={query.authors} + stats={facets.authors} + />} + <LanguageFacet + facetMode={query.facetMode} + onChange={this.props.onFilterChange} + onToggle={this.props.onFacetToggle} + open={!!openFacets.languages} + languages={query.languages} + referencedLanguages={this.props.referencedLanguages} + stats={facets.languages} + /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js new file mode 100644 index 00000000000..40bfb251141 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js @@ -0,0 +1,112 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { orderBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + statuses: Array<string> +|}; + +export default class StatusFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'statuses'; + + handleItemClick = (itemValue: string) => { + const { statuses } = this.props; + const newValue = orderBy( + statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(status: string): ?number { + const { stats } = this.props; + return stats ? stats[status] : null; + } + + renderStatus(status: string) { + return ( + <span> + <i className={`icon-status-${status.toLowerCase()}`} /> + {' '} + {translate('issue.status', status)} + </span> + ); + } + + renderItem = (status: string) => { + const active = this.props.statuses.includes(status); + const stat = this.getStat(status); + + return ( + <FacetItem + active={active} + disabled={stat === 0 && !active} + facetMode={this.props.facetMode} + halfWidth={true} + key={status} + name={this.renderStatus(status)} + onClick={this.handleItemClick} + stat={stat} + value={status} + /> + ); + }; + + render() { + const statuses = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED']; + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.statuses.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {statuses.map(this.renderItem)} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js new file mode 100644 index 00000000000..cd6ab46e679 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js @@ -0,0 +1,123 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { sortBy, uniq, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import FacetFooter from './components/FacetFooter'; +import { searchIssueTags } from '../../../api/issues'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + tags: Array<string> +|}; + +export default class TagFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'tags'; + + handleItemClick = (itemValue: string) => { + const { tags } = this.props; + const newValue = sortBy( + tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + handleSearch = (query: string) => { + return searchIssueTags({ ps: 50, q: query }).then(tags => + tags.map(tag => ({ label: tag, value: tag }))); + }; + + handleSelect = (tag: string) => { + const { tags } = this.props; + this.props.onChange({ [this.property]: uniq([...tags, tag]) }); + }; + + getStat(tag: string): ?number { + const { stats } = this.props; + return stats ? stats[tag] : null; + } + + renderTag(tag: string) { + return ( + <span> + <i className="icon-tags icon-gray little-spacer-right" /> + {tag} + </span> + ); + } + + render() { + const { stats } = this.props; + + if (!stats) { + return null; + } + + const tags = sortBy(Object.keys(stats), key => -stats[key]); + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.tags.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {tags.map(tag => ( + <FacetItem + active={this.props.tags.includes(tag)} + facetMode={this.props.facetMode} + key={tag} + name={this.renderTag(tag)} + onClick={this.handleItemClick} + stat={this.getStat(tag)} + value={tag} + /> + ))} + </FacetItemsList>} + + {this.props.open && + <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js new file mode 100644 index 00000000000..c0eb0271058 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { orderBy, without } from 'lodash'; +import FacetBox from './components/FacetBox'; +import FacetHeader from './components/FacetHeader'; +import FacetItem from './components/FacetItem'; +import FacetItemsList from './components/FacetItemsList'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + facetMode: string, + onChange: (changes: { [string]: Array<string> }) => void, + onToggle: (property: string) => void, + open: boolean, + stats?: { [string]: number }, + types: Array<string> +|}; + +export default class TypeFacet extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + property = 'types'; + + handleItemClick = (itemValue: string) => { + const { types } = this.props; + const newValue = orderBy( + types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + }; + + handleHeaderClick = () => { + this.props.onToggle(this.property); + }; + + getStat(type: string): ?number { + const { stats } = this.props; + return stats ? stats[type] : null; + } + + renderItem = (type: string) => { + const active = this.props.types.includes(type); + const stat = this.getStat(type); + + return ( + <FacetItem + active={active} + disabled={stat === 0 && !active} + facetMode={this.props.facetMode} + key={type} + name={<span><IssueTypeIcon query={type} /> {translate('issue.type', type)}</span>} + onClick={this.handleItemClick} + stat={stat} + value={type} + /> + ); + }; + + render() { + const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; + + return ( + <FacetBox property={this.property}> + <FacetHeader + hasValue={this.props.types.length > 0} + name={translate('issues.facet', this.property)} + onClick={this.handleHeaderClick} + open={this.props.open} + /> + + {this.props.open && + <FacetItemsList> + {types.map(this.renderItem)} + </FacetItemsList>} + </FacetBox> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js new file mode 100644 index 00000000000..5dc2230f4e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import AssigneeFacet from '../AssigneeFacet'; + +jest.mock('../../../../store/rootReducer', () => ({})); + +const renderAssigneeFacet = (props?: {}) => + shallow( + <AssigneeFacet + assigned={true} + assignees={[]} + facetMode="count" + onChange={jest.fn()} + onToggle={jest.fn()} + open={true} + referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} + stats={{ '': 5, foo: 13, bar: 7 }} + {...props} + /> + ); + +it('should render', () => { + expect(renderAssigneeFacet()).toMatchSnapshot(); +}); + +it('should not render without stats', () => { + expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot(); +}); + +it('should select unassigned', () => { + expect(renderAssigneeFacet({ assigned: false })).toMatchSnapshot(); +}); + +it('should select user', () => { + expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot(); +}); + +it('should render footer select option', () => { + const wrapper = renderAssigneeFacet(); + expect( + wrapper.instance().renderOption({ avatar: 'avatar-foo', label: 'name-foo' }) + ).toMatchSnapshot(); +}); + +it('should call onChange', () => { + const onChange = jest.fn(); + const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange }); + const itemOnClick = wrapper.find('FacetItem').first().prop('onClick'); + + itemOnClick(''); + expect(onChange).lastCalledWith({ assigned: false, assignees: [] }); + + itemOnClick('bar'); + expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar', 'foo'] }); + + itemOnClick('foo'); + expect(onChange).lastCalledWith({ assigned: true, assignees: [] }); +}); + +it('should call onToggle', () => { + const onToggle = jest.fn(); + const wrapper = renderAssigneeFacet({ onToggle }); + const headerOnClick = wrapper.find('FacetHeader').prop('onClick'); + + headerOnClick(); + expect(onToggle).lastCalledWith('assignees'); +}); + +it('should handle footer callbacks', () => { + const onChange = jest.fn(); + const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange }); + const onSelect = wrapper.find('FacetFooter').prop('onSelect'); + + onSelect('qux'); + expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] }); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js new file mode 100644 index 00000000000..a55c903b90c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import Sidebar from '../Sidebar'; + +jest.mock('../../../../store/rootReducer', () => ({})); + +const renderSidebar = props => + shallow(<Sidebar facets={{}} myIssues={false} openFacets={{}} query={{}} {...props} />) + .children() + .map(node => node.name()); + +it('should render all facets', () => { + expect(renderSidebar()).toMatchSnapshot(); +}); + +it('should render facets for project', () => { + expect(renderSidebar({ component: { qualifier: 'TRK' } })).toMatchSnapshot(); +}); + +it('should render facets for module', () => { + expect(renderSidebar({ component: { qualifier: 'BRC' } })).toMatchSnapshot(); +}); + +it('should render facets for directory', () => { + expect(renderSidebar({ component: { qualifier: 'DIR' } })).toMatchSnapshot(); +}); + +it('should render facets for developer', () => { + expect(renderSidebar({ component: { qualifier: 'DEV' } })).toMatchSnapshot(); +}); + +it('should render facets when my issues are selected', () => { + expect(renderSidebar({ myIssues: true })).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap new file mode 100644 index 00000000000..80726a28b0b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap @@ -0,0 +1,167 @@ +exports[`test should not render without stats 1`] = `null`; + +exports[`test should render 1`] = ` +<FacetBox + property="assignees"> + <FacetHeader + hasValue={false} + name="issues.facet.assignees" + onClick={[Function]} + open={true} /> + <FacetItemsList> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name="unassigned" + onClick={[Function]} + stat={5} + value="" /> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name={ + <span> + <Connect(Avatar) + className="little-spacer-right" + hash="avatart-foo" + size={16} /> + name-foo + </span> + } + onClick={[Function]} + stat={13} + value="foo" /> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name="bar" + onClick={[Function]} + stat={7} + value="bar" /> + </FacetItemsList> + <FacetFooter + onSearch={[Function]} + onSelect={[Function]} + renderOption={[Function]} /> +</FacetBox> +`; + +exports[`test should render footer select option 1`] = ` +<span> + <Connect(Avatar) + className="little-spacer-right" + hash="avatar-foo" + size={16} /> + name-foo +</span> +`; + +exports[`test should select unassigned 1`] = ` +<FacetBox + property="assignees"> + <FacetHeader + hasValue={true} + name="issues.facet.assignees" + onClick={[Function]} + open={true} /> + <FacetItemsList> + <FacetItem + active={true} + disabled={false} + facetMode="count" + halfWidth={false} + name="unassigned" + onClick={[Function]} + stat={5} + value="" /> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name={ + <span> + <Connect(Avatar) + className="little-spacer-right" + hash="avatart-foo" + size={16} /> + name-foo + </span> + } + onClick={[Function]} + stat={13} + value="foo" /> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name="bar" + onClick={[Function]} + stat={7} + value="bar" /> + </FacetItemsList> + <FacetFooter + onSearch={[Function]} + onSelect={[Function]} + renderOption={[Function]} /> +</FacetBox> +`; + +exports[`test should select user 1`] = ` +<FacetBox + property="assignees"> + <FacetHeader + hasValue={true} + name="issues.facet.assignees" + onClick={[Function]} + open={true} /> + <FacetItemsList> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name="unassigned" + onClick={[Function]} + stat={5} + value="" /> + <FacetItem + active={true} + disabled={false} + facetMode="count" + halfWidth={false} + name={ + <span> + <Connect(Avatar) + className="little-spacer-right" + hash="avatart-foo" + size={16} /> + name-foo + </span> + } + onClick={[Function]} + stat={13} + value="foo" /> + <FacetItem + active={false} + disabled={false} + facetMode="count" + halfWidth={false} + name="bar" + onClick={[Function]} + stat={7} + value="bar" /> + </FacetItemsList> + <FacetFooter + onSearch={[Function]} + onSelect={[Function]} + renderOption={[Function]} /> +</FacetBox> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap new file mode 100644 index 00000000000..81d6ce875fc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap @@ -0,0 +1,112 @@ +exports[`test should render all facets 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "ProjectFacet", + "ModuleFacet", + "DirectoryFacet", + "FileFacet", + "AssigneeFacet", + "AuthorFacet", + "LanguageFacet", +] +`; + +exports[`test should render facets for developer 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "ProjectFacet", + "ModuleFacet", + "DirectoryFacet", + "FileFacet", + "AssigneeFacet", + "LanguageFacet", +] +`; + +exports[`test should render facets for directory 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "FileFacet", + "AssigneeFacet", + "AuthorFacet", + "LanguageFacet", +] +`; + +exports[`test should render facets for module 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "ModuleFacet", + "DirectoryFacet", + "FileFacet", + "AssigneeFacet", + "AuthorFacet", + "LanguageFacet", +] +`; + +exports[`test should render facets for project 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "ModuleFacet", + "DirectoryFacet", + "FileFacet", + "AssigneeFacet", + "AuthorFacet", + "LanguageFacet", +] +`; + +exports[`test should render facets when my issues are selected 1`] = ` +Array [ + "FacetMode", + "TypeFacet", + "ResolutionFacet", + "SeverityFacet", + "StatusFacet", + "CreationDateFacet", + "RuleFacet", + "TagFacet", + "ProjectFacet", + "ModuleFacet", + "DirectoryFacet", + "FileFacet", + "AuthorFacet", + "LanguageFacet", +] +`; diff --git a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js index a2f31cb9c37..358f6ee3a19 100644 --- a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js @@ -17,16 +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 BaseFacet from './base-facet'; -import Template from '../templates/facets/issues-context-facet.hbs'; +// @flow +import React from 'react'; -export default BaseFacet.extend({ - template: Template, +type Props = {| + children?: React.Element<*>, + property: string +|}; - serializeData() { - return { - ...BaseFacet.prototype.serializeData.apply(this, arguments), - state: this.options.app.state.toJSON() - }; - } -}); +const FacetBox = (props: Props) => ( + <div className="search-navigator-facet-box" data-property={props.property}> + {props.children} + </div> +); + +export default FacetBox; diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js index c2194ad5409..8f0fca20fdc 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js @@ -17,15 +17,26 @@ * 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 { translate } from '../../../helpers/l10n'; +import SearchSelect from '../../components/SearchSelect'; + +type Option = { label: string, value: string }; + +type Props = {| + minimumQueryLength?: number, + onSearch: (query: string) => Promise<Array<Option>>, + onSelect: (value: string) => void, + renderOption?: (option: Object) => React.Element<*> +|}; + +export default class FacetFooter extends React.PureComponent { + props: Props; -export default class NoProjects extends React.Component { render() { return ( - <div className="projects-empty-list"> - <h3>{translate('projects.no_projects.1')}</h3> - <p className="big-spacer-top">{translate('projects.no_projects.2')}</p> + <div className="search-navigator-facet-footer"> + <SearchSelect {...this.props} /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js new file mode 100644 index 00000000000..ff6ef8387c9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +/* eslint-disable max-len */ +import React from 'react'; + +type Props = { + hasValue: boolean, + name: string, + onClick?: () => void, + open: boolean +}; + +export default class FacetHeader extends React.PureComponent { + props: Props; + + static defaultProps = { + hasValue: false, + open: true + }; + + handleClick = (e: Event & { currentTarget: HTMLElement }) => { + e.preventDefault(); + e.currentTarget.blur(); + if (this.props.onClick) { + this.props.onClick(); + } + }; + + renderCheckbox() { + return ( + <svg viewBox="0 0 1792 1792" width="10" height="10" style={{ paddingTop: 3 }}> + {this.props.open + ? <path + style={{ fill: 'currentColor ' }} + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + /> + : <path + style={{ fill: 'currentColor ' }} + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + />} + </svg> + ); + } + + renderValueIndicator() { + return this.props.hasValue && !this.props.open + ? <svg viewBox="0 0 1792 1792" width="8" height="8" style={{ paddingTop: 5, paddingLeft: 8 }}> + <path + d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z" + fill="#4b9fd5" + /> + </svg> + : null; + } + + render() { + return this.props.onClick + ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}> + {this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()} + </a> + : <span className="search-navigator-facet-header"> + {this.props.name} + </span>; + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js new file mode 100644 index 00000000000..94d512d195b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import { formatMeasure } from '../../../../helpers/measures'; + +type Props = {| + active: boolean, + disabled: boolean, + facetMode: string, + halfWidth: boolean, + name: string | React.Element<*>, + onClick: (string) => void, + stat: ?number, + value: string +|}; + +export default class FacetItem extends React.PureComponent { + props: Props; + + static defaultProps = { + disabled: false, + halfWidth: false + }; + + handleClick = (event: Event & { currentTarget: HTMLElement }) => { + event.preventDefault(); + const value = event.currentTarget.dataset.value; + this.props.onClick(value); + }; + + render() { + const { stat } = this.props; + + const className = classNames('facet', 'search-navigator-facet', { + active: this.props.active, + 'search-navigator-facet-half': this.props.halfWidth + }); + + const formattedStat = stat && + formatMeasure(stat, this.props.facetMode === 'effort' ? 'SHORT_WORK_DUR' : 'SHORT_INT'); + + return this.props.disabled + ? <span className={className}> + <span className="facet-name">{this.props.name}</span> + <span className="facet-stat">{formattedStat}</span> + </span> + : <a className={className} data-value={this.props.value} href="#" onClick={this.handleClick}> + <span className="facet-name">{this.props.name}</span> + <span className="facet-stat">{formattedStat}</span> + </a>; + } +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js new file mode 100644 index 00000000000..4a203f0c071 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; + +type Props = {| + children?: Array<React.Element<*>> +|}; + +const FacetItemsList = (props: Props) => ( + <div className="search-navigator-facet-list"> + {props.children} + </div> +); + +export default FacetItemsList; diff --git a/server/sonar-web/src/main/js/apps/component-issues/routes.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js index 954ba1487bb..0eb453e8066 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/routes.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js @@ -17,15 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const routes = [ - { - indexRoute: { - getComponent(_, callback) { - require.ensure([], require => - callback(null, require('./components/ComponentIssuesAppContainer').default)); - } - } - } -]; +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import FacetBox from '../FacetBox'; -export default routes; +it('should render', () => { + expect(shallow(<FacetBox property="foo"><div /></FacetBox>)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js index eea72dc3e28..4dbf1cc3ece 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js @@ -17,13 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import Marionette from 'backbone.marionette'; -import { translate } from '../../helpers/l10n'; +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import FacetFooter from '../FacetFooter'; -export default Marionette.ItemView.extend({ - className: 'search-navigator-no-results', - - template() { - return translate('issue_filter.no_issues'); - } +it('should render', () => { + expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js new file mode 100644 index 00000000000..5aa00c4a41e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../../helpers/testUtils'; +import FacetHeader from '../FacetHeader'; + +it('should render open facet with value', () => { + expect( + shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />) + ).toMatchSnapshot(); +}); + +it('should render open facet without value', () => { + expect( + shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />) + ).toMatchSnapshot(); +}); + +it('should render closed facet with value', () => { + expect( + shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={false} />) + ).toMatchSnapshot(); +}); + +it('should render closed facet without value', () => { + expect( + shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />) + ).toMatchSnapshot(); +}); + +it('should render without link', () => { + expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot(); +}); + +it('should call onClick', () => { + const onClick = jest.fn(); + const wrapper = shallow( + <FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} /> + ); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js new file mode 100644 index 00000000000..ddc84eca458 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../../helpers/testUtils'; +import FacetItem from '../FacetItem'; + +const renderFacetItem = (props: {}) => + shallow( + <FacetItem + active={false} + facetMode="count" + name="foo" + onClick={jest.fn()} + stat={null} + value="bar" + {...props} + /> + ); + +it('should render active', () => { + expect(renderFacetItem({ active: true })).toMatchSnapshot(); +}); + +it('should render inactive', () => { + expect(renderFacetItem({ active: false })).toMatchSnapshot(); +}); + +it('should render stat', () => { + expect(renderFacetItem({ stat: 13 })).toMatchSnapshot(); +}); + +it('should render disabled', () => { + expect(renderFacetItem({ disabled: true })).toMatchSnapshot(); +}); + +it('should render half width', () => { + expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot(); +}); + +it('should render effort stat', () => { + expect(renderFacetItem({ facetMode: 'effort', stat: 1234 })).toMatchSnapshot(); +}); + +it('should call onClick', () => { + const onClick = jest.fn(); + const wrapper = renderFacetItem({ onClick }); + click(wrapper, { currentTarget: { dataset: { value: 'bar' } } }); + expect(onClick).toHaveBeenCalled(); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/models/issue.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js index 218e27810ad..883e62b88d4 100644 --- a/server/sonar-web/src/main/js/apps/issues/models/issue.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js @@ -17,14 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import Issue from '../../../components/issue/models/issue'; +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import FacetItemsList from '../FacetItemsList'; -export default Issue.extend({ - reset(attrs, options) { - const keepFields = ['index', 'selected', 'comments']; - keepFields.forEach(field => { - attrs[field] = this.get(field); - }); - return Issue.prototype.reset.call(this, attrs, options); - } +it('should render', () => { + expect(shallow(<FacetItemsList><div /></FacetItemsList>)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap new file mode 100644 index 00000000000..2722912ca7d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap @@ -0,0 +1,7 @@ +exports[`test should render 1`] = ` +<div + className="search-navigator-facet-box" + data-property="foo"> + <div /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap new file mode 100644 index 00000000000..21f8096c8e8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap @@ -0,0 +1,10 @@ +exports[`test should render 1`] = ` +<div + className="search-navigator-facet-footer"> + <SearchSelect + minimumQueryLength={2} + onSearch={[Function]} + onSelect={[Function]} + resetOnBlur={true} /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap new file mode 100644 index 00000000000..3333ae8944d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap @@ -0,0 +1,132 @@ +exports[`test should render closed facet with value 1`] = ` +<a + className="search-navigator-facet-header" + href="#" + onClick={[Function]}> + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10"> + <path + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + style={ + Object { + "fill": "currentColor ", + } + } /> + </svg> + + foo + + <svg + height="8" + style={ + Object { + "paddingLeft": 8, + "paddingTop": 5, + } + } + viewBox="0 0 1792 1792" + width="8"> + <path + d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z" + fill="#4b9fd5" /> + </svg> +</a> +`; + +exports[`test should render closed facet without value 1`] = ` +<a + className="search-navigator-facet-header" + href="#" + onClick={[Function]}> + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10"> + <path + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + style={ + Object { + "fill": "currentColor ", + } + } /> + </svg> + + foo + +</a> +`; + +exports[`test should render open facet with value 1`] = ` +<a + className="search-navigator-facet-header" + href="#" + onClick={[Function]}> + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10"> + <path + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + style={ + Object { + "fill": "currentColor ", + } + } /> + </svg> + + foo + +</a> +`; + +exports[`test should render open facet without value 1`] = ` +<a + className="search-navigator-facet-header" + href="#" + onClick={[Function]}> + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10"> + <path + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + style={ + Object { + "fill": "currentColor ", + } + } /> + </svg> + + foo + +</a> +`; + +exports[`test should render without link 1`] = ` +<span + className="search-navigator-facet-header"> + foo +</span> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap new file mode 100644 index 00000000000..0a22b710499 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap @@ -0,0 +1,90 @@ +exports[`test should render active 1`] = ` +<a + className="facet search-navigator-facet active" + data-value="bar" + href="#" + onClick={[Function]}> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat" /> +</a> +`; + +exports[`test should render disabled 1`] = ` +<span + className="facet search-navigator-facet"> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat" /> +</span> +`; + +exports[`test should render effort stat 1`] = ` +<a + className="facet search-navigator-facet" + data-value="bar" + href="#" + onClick={[Function]}> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat"> + work_duration.x_days.3 + </span> +</a> +`; + +exports[`test should render half width 1`] = ` +<a + className="facet search-navigator-facet search-navigator-facet-half" + data-value="bar" + href="#" + onClick={[Function]}> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat" /> +</a> +`; + +exports[`test should render inactive 1`] = ` +<a + className="facet search-navigator-facet" + data-value="bar" + href="#" + onClick={[Function]}> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat" /> +</a> +`; + +exports[`test should render stat 1`] = ` +<a + className="facet search-navigator-facet" + data-value="bar" + href="#" + onClick={[Function]}> + <span + className="facet-name"> + foo + </span> + <span + className="facet-stat"> + 13 + </span> +</a> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap new file mode 100644 index 00000000000..15686ac81d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap @@ -0,0 +1,6 @@ +exports[`test should render 1`] = ` +<div + className="search-navigator-facet-list"> + <div /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css deleted file mode 100644 index 46942bb2796..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ /dev/null @@ -1,23 +0,0 @@ -.issues-header-inner { - padding: 32px 10px 15px; - background-color: #f3f3f3; - text-align: center; -} - -.issues-header-inner:empty { - display: none; -} - -.issues-header-order { - display: inline-block; - vertical-align: top; - margin-right: 20px; - font-size: 12px; -} - -.issues-header-order { - display: inline-block; - vertical-align: top; - margin-right: 20px; - font-size: 12px; -} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs b/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs deleted file mode 100644 index fda291ab2b3..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs +++ /dev/null @@ -1,145 +0,0 @@ -{{#if isLoaded}} - <form id="bulk-change-form"> - <div class="modal-head"> - <h2>{{tp 'issue_bulk_change.form.title' issues.length}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - - {{#if limitReached}} - <div class="alert alert-warning"> - {{tp 'issue_bulk_change.max_issues_reached' issues.length}} - </div> - {{/if}} - - {{! assign }} - {{#if canBeAssigned}} - <div class="modal-field"> - <label for="assignee">{{t 'issue.assign.formlink'}}</label> - <input id="assign-action" name="actions[]" type="checkbox" value="assign"> - <input id="assignee" type="hidden"> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' canBeAssigned}}) - </div> - </div> - {{/if}} - - {{! type }} - {{#if canChangeType}} - <div class="modal-field"> - <label for="type">{{t 'issue.set_type'}}</label> - <input id="set-type-action" name="actions[]" type="checkbox" value="set_type"> - <select id="type" name="set_type.type"> - <option value="BUG">{{t 'issue.type.BUG'}}</option> - <option value="VULNERABILITY">{{t 'issue.type.VULNERABILITY'}}</option> - <option value="CODE_SMELL">{{t 'issue.type.CODE_SMELL'}}</option> - </select> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' canChangeType}}) - </div> - </div> - {{/if}} - - {{! severity }} - {{#if canChangeSeverity}} - <div class="modal-field"> - <label for="severity">{{t 'issue.set_severity'}}</label> - <input id="set-severity-action" name="actions[]" type="checkbox" value="set_severity"> - <select id="severity" name="set_severity.severity"> - <option value="BLOCKER">{{t 'severity.BLOCKER'}}</option> - <option value="CRITICAL">{{t 'severity.CRITICAL'}}</option> - <option value="MAJOR">{{t 'severity.MAJOR'}}</option> - <option value="MINOR">{{t 'severity.MINOR'}}</option> - <option value="INFO">{{t 'severity.INFO'}}</option> - </select> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' canChangeSeverity}}) - </div> - </div> - {{/if}} - - {{! add tags }} - {{#if canChangeTags}} - <div class="modal-field"> - <label for="add_tags">{{t 'issue.add_tags'}}</label> - <input id="add-tags-action" name="actions[]" type="checkbox" value="add_tags"> - <input id="add_tags" name="add_tags.tags" type="text"> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' canChangeTags}}) - </div> - </div> - {{/if}} - - {{! remove tags }} - {{#if canChangeTags}} - <div class="modal-field"> - <label for="remove_tags">{{t 'issue.remove_tags'}}</label> - <input id="remove-tags-action" name="actions[]" type="checkbox" value="remove_tags"> - <input id="remove_tags" name="remove_tags.tags" type="text"> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' canChangeTags}}) - </div> - </div> - {{/if}} - - {{! transitions }} - {{#notEmpty availableTransitions}} - <div class="modal-field"> - <label>{{t 'issue.transition'}}</label> - {{#each availableTransitions}} - <input type="radio" id="transition-{{transition}}" name="do_transition.transition" - value="{{transition}}"> - <label for="transition-{{transition}}" style="float: none; display: inline; left: 0; cursor: pointer;"> - {{t 'issue.transition' transition}} - </label> - <div class="pull-right note"> - ({{tp 'issue_bulk_change.x_issues' count}}) - </div> - <br> - {{/each}} - </div> - {{/notEmpty}} - - {{! comment }} - {{#if canBeCommented}} - <div class="modal-field"> - <label for="comment"> - {{t 'issue.comment.formlink'}} - <i class="icon-help" title="{{t 'issue_bulk_change.comment.help'}}"></i> - </label> - <div> - <textarea rows="4" name="comment" id="comment" style="width: 100%"></textarea> - </div> - <div class="pull-right"> - {{> ../../../components/common/templates/_markdown-tips}} - </div> - </div> - {{/if}} - - {{! notifications }} - <div class="modal-field"> - <label for="send-notifications">{{t 'issue.send_notifications'}}</label> - <input id="send-notifications" name="sendNotifications" type="checkbox" value="true"> - </div> - - </div> - <div class="modal-foot"> - <i class="js-modal-spinner spinner spacer-right hidden"></i> - <button id="bulk-change-submit">{{t 'apply'}}</button> - <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a> - </div> - </form> -{{else}} - <div class="modal-head"> - <h2>{{t 'bulk_change'}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - <div class="text-center"> - <i class="spinner spinner-margin"></i> - </div> - </div> - <div class="modal-foot"> - <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a> - </div> -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs deleted file mode 100644 index 64237bc0e79..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<a class="search-navigator-facet-header js-facet-toggle"> - <i class="icon-checkbox {{#if enabled}}icon-checkbox-checked{{/if}}"></i> - {{t "issues.facet" property}} -</a> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs deleted file mode 100644 index 9fb23f78ab8..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - {{#eq val ""}} - {{! unassigned }} - <a class="facet search-navigator-facet js-facet" data-unassigned title="{{t "unassigned"}}"> - <span class="facet-name">{{t "unassigned"}}</span> - <span class="facet-stat"> - {{formatFacetValue count ../../state.facetMode}} - </span> - </a> - {{else}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{label}}"> - <span class="facet-name">{{label}}</span> - <span class="facet-stat"> - {{formatFacetValue count ../../state.facetMode}} - </span> - </a> - {{/eq}} - {{/each}} - - <div class="search-navigator-facet-custom-value"> - <input type="hidden" class="js-custom-value"> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs deleted file mode 100644 index 83442f81b37..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{> "_issues-facet-header"}} -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}</span> - <span class="facet-stat"> - {{formatFacetValue count ../state.facetMode}} - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs deleted file mode 100644 index 9f981c07c1a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="search-navigator-facet-query"> - Issues of {{qualifierIcon state.contextComponentQualifier}} {{state.contextComponentName}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs deleted file mode 100644 index bf5410833c9..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs +++ /dev/null @@ -1,42 +0,0 @@ -{{> "_issues-facet-header"}} - -{{#if createdAt}} - <input type="hidden" name="createdAt"> - <div class="search-navigator-facet-container"> - {{dt createdAt}} ({{fromNow createdAt}}) - </div> -{{else}} - <div class="search-navigator-facet-container"> - <div class="js-barchart" data-height="75" {{#if periodEnd}}data-end-date="{{periodEnd}}"{{/if}}></div> - <div class="search-navigator-date-facet-selection"> - <a class="js-select-period-start search-navigator-date-facet-selection-dropdown-left"> - {{#if periodStart}}{{d periodStart}}{{else}}Past{{/if}} <i class="icon-dropdown"></i> - </a> - <a class="js-select-period-end search-navigator-date-facet-selection-dropdown-right"> - {{#if periodEnd}}{{d periodEnd}}{{else}}Now{{/if}} <i class="icon-dropdown"></i> - </a> - <input class="js-period-start search-navigator-date-facet-selection-input-left" - type="text" value="{{#if periodStart}}{{ds periodStart}}{{/if}}" name="createdAfter"> - <input class="js-period-end search-navigator-date-facet-selection-input-right" - type="text" value="{{#if periodEnd}}{{ds periodEnd}}{{/if}}" name="createdBefore"> - </div> - - <div class="spacer-top"> - <span class="spacer-right">{{t "issues.facet.createdAt.or"}}</span> - <a class="js-all spacer-right" href="#">{{t "issues.facet.createdAt.all"}}</a> - {{#if hasLeak}} - <a class="js-leak spacer-right {{#eq sinceLeakPeriod "true"}}active-link{{/eq}}" href="#">Leak Period</a> - {{else}} - <a class="js-last-week spacer-right {{#eq createdInLast "1w"}}active-link{{/eq}}" href="#"> - {{t "issues.facet.createdAt.last_week"}} - </a> - <a class="js-last-month spacer-right {{#eq createdInLast "1m"}}active-link{{/eq}}" href="#"> - {{t "issues.facet.createdAt.last_month"}} - </a> - <a class="js-last-year {{#eq createdInLast "1y"}}active-link{{/eq}}" href="#"> - {{t "issues.facet.createdAt.last_year"}} - </a> - {{/if}} - </div> - </div> -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs deleted file mode 100644 index 0674f5b87bb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}"> - <span class="facet-name">{{default label val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} - - <div class="search-navigator-facet-custom-value"> - <input type="hidden" class="js-custom-value"> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs deleted file mode 100644 index 3569040b69c..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list search-navigator-facet-list-align-right"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs deleted file mode 100644 index 28ae140f75b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-container"> - <div class="facet search-navigator-facet active" style="cursor: default;"> - <span class="facet-name">{{issues}}</span> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs deleted file mode 100644 index af69286d92a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs +++ /dev/null @@ -1,15 +0,0 @@ -<div class="search-navigator-facet-header"> - {{t 'issues.facet.mode'}} -</div> - - -<div class="search-navigator-facet-list"> - <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'count'}}active{{/eq}}" - data-value="count"> - <span class="facet-name">{{t 'issues.facet.mode.issues'}}</span> - </a> - <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'effort'}}active{{/eq}}" - data-value="effort"> - <span class="facet-name">{{t 'issues.facet.mode.effort'}}</span> - </a> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs deleted file mode 100644 index c11181a70dd..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if user.isLoggedIn}} - <ul class="radio-toggle"> - <li> - <input type="radio" name="issues-page-my" value="my" id="issues-page-my-my" {{#if me}}checked{{/if}}> - <label for="issues-page-my-my">My Issues</label> - </li> - <li> - <input type="radio" name="issues-page-my" value="all" id="issues-page-my-all" {{#unless me}}checked{{/unless}}> - <label for="issues-page-my-all">All</label> - </li> - </ul> -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs deleted file mode 100644 index 7693af65549..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}"> - <span class="facet-name">{{#if organization}}{{organization.name}}<span class="slash-separator"></span>{{/if}}{{default label val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} - - <div class="search-navigator-facet-custom-value"> - <input type="hidden" class="js-custom-value"> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs deleted file mode 100644 index 19e47071f8b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - {{#eq val ""}} - {{! unresolved }} - <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-unresolved - title="{{t "issue.unresolved.description"}}" data-toggle="tooltip" data-placement="right"> - <span class="facet-name">{{t "unresolved"}}</span> - <span class="facet-stat"> - {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{else}} - <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-value="{{val}}" - title="{{t "issue.resolution" val "description"}}" data-toggle="tooltip" data-placement="right"> - <span class="facet-name">{{t "issue.resolution" val}}</span> - <span class="facet-stat"> - {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/eq}} - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs deleted file mode 100644 index 88b9bd0585b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet search-navigator-facet-half js-facet" - data-value="{{val}}" title="{{t "severity" val "description"}}" data-toggle="tooltip" data-placement="right"> - <span class="facet-name">{{severityIcon val}} {{t "severity" val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs deleted file mode 100644 index cc7f1fcefdb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet search-navigator-facet-half js-facet" - data-value="{{val}}" title="{{t "issue.status" val "description"}}" data-toggle="tooltip" data-placement="right"> - <span class="facet-name">{{statusIcon val}} {{t "issue.status" val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs deleted file mode 100644 index f97ada41dca..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> "_issues-facet-header"}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" - data-value="{{val}}"> - <span class="facet-name">{{issueTypeIcon val}} {{t 'issue.type' val}}</span> - <span class="facet-stat"> - {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}} - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs deleted file mode 100644 index f18f6845d3a..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs +++ /dev/null @@ -1,89 +0,0 @@ -<header class="menu-search"> - <h6>{{t "issue.filter_similar_issues"}}</h6> -</header> - -<ul class="menu"> - <li> - <a href="#" data-property="types" data-value="{{this.type}}"> - {{issueType this.type}} - </a> - </li> - - <li> - <a href="#" data-property="severities" data-value="{{s}}"> - {{severityIcon severity}} {{t "severity" severity}} - </a> - </li> - - <li> - <a href="#" data-property="statuses" data-value="{{status}}"> - {{statusIcon status}} {{t "issue.status" status}} - </a> - </li> - - <li> - {{#if resolution}} - <a href="#" data-property="resolutions" data-value="{{resolution}}"> - {{t "issue.resolution" resolution}} - </a> - {{else}} - <a href="#" data-property="resolved" data-value="false"> - {{t "unresolved"}} - </a> - {{/if}} - </li> - - <li> - {{#if assignee}} - <a href="#" class="issue-action-option" data-property="assignees" data-value="{{assignee}}"> - {{t "assigned_to"}} - {{#ifShowAvatars}}<span class="spacer-left">{{avatarHelperNew assigneeAvatar 16}}</span>{{/ifShowAvatars}} - {{assigneeName}} - </a> - {{else}} - <a href="#" data-property="assigned" data-value="false"> - {{t "unassigned"}} - </a> - {{/if}} - </li> - - <li class="divider"></li> - - <li> - <a href="#" data-property="rules" data-value="{{rule}}"> - {{limitString ruleName}} - </a> - </li> - - {{#each tags}} - <li> - <a href="#" data-property="tags" data-value="{{this}}"> - <i class="icon-tags icon-half-transparent"></i> {{this}} - </a> - </li> - {{/each}} - - <li class="divider"></li> - - <li> - <a href="#" data-property="projectUuids" data-value="{{projectUuid}}"> - {{qualifierIcon "TRK"}} {{projectLongName}} - </a> - </li> - - {{#if subProject}} - <li> - <a href="#" data-property="moduleUuids" data-value="{{subProjectUuid}}"> - {{qualifierIcon "BRC"}} {{subProjectLongName}} - </a> - </li> - {{/if}} - - <li> - <a href="#" data-property="fileUuids" data-value="{{componentUuid}}"> - {{qualifierIcon componentQualifier}} {{fileFromPath componentLongName}} - </a> - </li> -</ul> - -<div class="bubble-popup-arrow"></div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs deleted file mode 100644 index 83add61b366..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="issues search-navigator"> - <div class="search-navigator-side"> - <div class="issues-header"></div> - <div class="search-navigator-facets"></div> - </div> - - <div class="search-navigator-workspace"> - <div class="search-navigator-workspace-header"></div> - <div class="search-navigator-workspace-list"></div> - <div class="issues-workspace-component-viewer"></div> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs deleted file mode 100644 index 6a28b925ad8..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs +++ /dev/null @@ -1,80 +0,0 @@ -<div class="issues-header-component nowrap"> - <div class="component-name"> - {{#if state.component}} - - <div class="component-name-parent"> - <a class="js-back">{{t "issues.return_to_list"}}</a> - </div> - - <div class="component-name-parent"> - {{#if organization}} - <a href="{{link '/organizations/' organization.key}}">{{organization.name}}</a> - <span class="slash-separator"></span> - {{/if}} - {{#with state.component}} - {{#if project}} - <a href="{{dashboardUrl project}}" title="{{projectName}}">{{projectName}}</a> - <span class="slash-separator"></span> - {{/if}} - {{#if subProject}} - <a href="{{dashboardUrl subProject}}" title="{{subProjectName}}">{{subProjectName}}</a> - <span class="slash-separator"></span> - {{/if}} - <a href="{{dashboardUrl key}}" title="{{name}}">{{collapsePath name}}</a> - {{/with}} - </div> - - {{else}} - {{#if state.canBulkChange}} - <a class="js-selection icon-checkbox {{#if allSelected}}icon-checkbox-checked{{/if}} {{#if someSelected}}icon-checkbox-checked icon-checkbox-single{{/if}}" - data-toggle="tooltip" title="{{t 'issues.toggle_selection_tooltip'}}"></a> - {{else}} - - {{/if}} - {{/if}} - </div> -</div> - - -<div class="search-navigator-header-actions"> - {{#notNull state.total}} - {{#unless state.component}} - <div class="issues-header-order">{{t 'issues.ordered'}} <strong>{{t 'issues.by_creation_date'}}</strong></div> - {{/unless}} - <div class="search-navigator-header-pagination flash flash-heavy"> - {{#gt state.total 0}} - <strong> - <span class="current"> - {{sum state.selectedIndex 1}} - / - <span id="issues-total">{{formatMeasure state.total 'INT'}}</span> - </span> - </strong> - {{else}} - <span class="current">0 / <span id="issues-total">0</span></span> - {{/gt}} - {{t 'issues.issues'}} - </div> - {{/notNull}} - - - <div class="search-navigator-header-buttons button-group dropdown"> - <button id="issues-reload" class="js-reload">{{t "reload"}}</button> - <button class="js-new-search" id="issues-new-search">{{t "issue_filter.new_search"}}</button> - {{#if state.canBulkChange}} - <button id="issues-bulk-change" class="dropdown-toggle" data-toggle="dropdown"> - {{t "bulk_change"}} - </button> - <ul class="dropdown-menu dropdown-menu-right"> - <li> - <a class="js-bulk-change" href="#">{{tp 'issues.bulk_change' state.total}}</a> - </li> - {{#gt selectedCount 0}} - <li> - <a class="js-bulk-change-selected" href="#">{{tp 'issues.bulk_change_selected' selectedCount}}</a> - </li> - {{/gt}} - </ul> - {{/if}} - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs deleted file mode 100644 index b0e305ecddb..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<div class="component-name issues-workspace-list-component"> - {{#if organization}} - <a class="link-no-underline" href="{{link '/organizations/' organization.key}}"> - {{organization.name}} - </a> - <span class="slash-separator"></span> - {{/if}} - - {{#if project}} - <a class="link-no-underline" href="{{dashboardUrl project}}"> - {{projectLongName}} - </a> - <span class="slash-separator"></span> - {{/if}} - - {{#if subProject}} - <a class="link-no-underline" href="{{dashboardUrl subProject}}"> - {{subProjectLongName}} - </a> - <span class="slash-separator"></span> - {{/if}} - - <a class="link-no-underline" href="{{dashboardUrl component}}"> - {{collapsePath componentLongName}} - </a> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs deleted file mode 100644 index cae9c964a0b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="js-list"></div> - -<div class="search-navigator-workspace-list-more"> - <span class="js-more"><i class="spinner"></i></span> -</div> diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js new file mode 100644 index 00000000000..3c9d8a235ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/utils.js @@ -0,0 +1,229 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import { isNil, omitBy } from 'lodash'; +import { searchMembers } from '../../api/organizations'; +import { searchUsers } from '../../api/users'; + +export type RawQuery = { [string]: string }; + +export type Query = {| + assigned: boolean, + assignees: Array<string>, + authors: Array<string>, + createdAfter: string, + createdAt: string, + createdBefore: string, + createdInLast: string, + directories: Array<string>, + facetMode: string, + files: Array<string>, + issues: Array<string>, + languages: Array<string>, + modules: Array<string>, + projects: Array<string>, + resolved: boolean, + resolutions: Array<string>, + rules: Array<string>, + severities: Array<string>, + sinceLeakPeriod: boolean, + statuses: Array<string>, + tags: Array<string>, + types: Array<string> +|}; + +export type Paging = { + pageIndex: number, + pageSize: number, + total: number +}; + +const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean => + value === 'false' ? false : value === 'true' ? true : defaultValue; + +const parseAsString = (value: ?string): string => value || ''; + +const parseAsStringArray = (value: ?string): Array<string> => value ? value.split(',') : []; + +const parseAsFacetMode = (facetMode: string) => + facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'; + +export const parseQuery = (query: RawQuery): Query => ({ + assigned: parseAsBoolean(query.assigned), + assignees: parseAsStringArray(query.assignees), + authors: parseAsStringArray(query.authors), + createdAfter: parseAsString(query.createdAfter), + createdAt: parseAsString(query.createdAt), + createdBefore: parseAsString(query.createdBefore), + createdInLast: parseAsString(query.createdInLast), + directories: parseAsStringArray(query.directories), + facetMode: parseAsFacetMode(query.facetMode), + files: parseAsStringArray(query.fileUuids), + issues: parseAsStringArray(query.issues), + languages: parseAsStringArray(query.languages), + modules: parseAsStringArray(query.moduleUuids), + projects: parseAsStringArray(query.projectUuids), + resolved: parseAsBoolean(query.resolved), + resolutions: parseAsStringArray(query.resolutions), + rules: parseAsStringArray(query.rules), + severities: parseAsStringArray(query.severities), + sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false), + statuses: parseAsStringArray(query.statuses), + tags: parseAsStringArray(query.tags), + types: parseAsStringArray(query.types) +}); + +export const getOpen = (query: RawQuery) => query.open; + +export const areMyIssuesSelected = (query: RawQuery): boolean => query.myIssues === 'true'; + +const serializeString = (value: string): ?string => value || undefined; + +const serializeValue = (value: Array<string>): ?string => value.length ? value.join() : undefined; + +export const serializeQuery = (query: Query): RawQuery => { + const filter = { + assigned: query.assigned ? undefined : 'false', + assignees: serializeValue(query.assignees), + authors: serializeValue(query.authors), + createdAfter: serializeString(query.createdAfter), + createdAt: serializeString(query.createdAt), + createdBefore: serializeString(query.createdBefore), + createdInLast: serializeString(query.createdInLast), + directories: serializeValue(query.directories), + facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined, + fileUuids: serializeValue(query.files), + issues: serializeValue(query.issues), + languages: serializeValue(query.languages), + moduleUuids: serializeValue(query.modules), + projectUuids: serializeValue(query.projects), + resolved: query.resolved ? undefined : 'false', + resolutions: serializeValue(query.resolutions), + severities: serializeValue(query.severities), + sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined, + statuses: serializeValue(query.statuses), + rules: serializeValue(query.rules), + tags: serializeValue(query.tags), + types: serializeValue(query.types) + }; + return omitBy(filter, isNil); +}; + +const areArraysEqual = (a: Array<string>, b: Array<string>) => { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +}; + +export const areQueriesEqual = (a: RawQuery, b: RawQuery) => { + const parsedA: Query = parseQuery(a); + const parsedB: Query = parseQuery(b); + + const keysA = Object.keys(parsedA); + const keysB = Object.keys(parsedB); + + if (keysA.length !== keysB.length) { + return false; + } + + return keysA.every( + key => + Array.isArray(parsedA[key]) && Array.isArray(parsedB[key]) + ? areArraysEqual(parsedA[key], parsedB[key]) + : parsedA[key] === parsedB[key] + ); +}; + +type RawFacet = { + property: string, + values: Array<{ val: string, count: number }> +}; + +export type Facet = { [string]: number }; + +export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => { + // for readability purpose + const propertyMapping = { + fileUuids: 'files', + moduleUuids: 'modules', + projectUuids: 'projects' + }; + + const result = {}; + facets.forEach(facet => { + const values = {}; + facet.values.forEach(value => { + values[value.val] = value.count; + }); + const finalProperty = propertyMapping[facet.property] || facet.property; + result[finalProperty] = values; + }); + return result; +}; + +export type ReferencedComponent = { + key: string, + name: string, + organization: string, + path: string +}; + +export type ReferencedUser = { + avatar: string, + name: string +}; + +export type ReferencedLanguage = { + name: string +}; + +export type Component = { + key: string, + organization: string, + qualifier: string +}; + +export type CurrentUser = + | { isLoggedIn: false } + | { isLoggedIn: true, email?: string, login: string, name: string }; + +export const searchAssignees = (query: string, component?: Component) => { + return component + ? searchMembers({ organization: component.organization, ps: 50, q: query }).then(response => + response.users.map(user => ({ + avatar: user.avatar, + label: user.name, + value: user.login + }))) + : searchUsers(query, 50).then(response => + response.users.map(user => ({ + // TODO this WS returns no avatar + avatar: user.avatar, + email: user.email, + label: user.name, + value: user.login + }))); +}; diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js deleted file mode 100644 index 0fccbb3ab65..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import WorkspaceHeaderView from '../../components/navigator/workspace-header-view'; -import BulkChangeForm from './BulkChangeForm'; -import Template from './templates/issues-workspace-header.hbs'; -import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils'; - -export default WorkspaceHeaderView.extend({ - template: Template, - - initialize() { - this.context = { - isContext: this.options.app.state.get('isContext'), - organization: this.options.app.state.get('contextOrganization') - }; - }, - - events() { - return { - ...WorkspaceHeaderView.prototype.events.apply(this, arguments), - 'click .js-selection': 'onSelectionClick', - 'click .js-back': 'returnToList', - 'click .js-new-search': 'newSearch', - 'click .js-bulk-change-selected': 'onBulkChangeSelectedClick' - }; - }, - - onSelectionClick(e) { - e.preventDefault(); - this.toggleSelection(); - }, - - onBulkChangeSelectedClick(e) { - e.preventDefault(); - this.bulkChangeSelected(); - }, - - afterBulkChange() { - const selectedIndex = this.options.app.state.get('selectedIndex'); - const selectedKeys = this.options.app.list.where({ selected: true }).map(item => item.id); - this.options.app.controller.fetchList().done(() => { - this.options.app.state.set({ selectedIndex }); - this.options.app.list.selectByKeys(selectedKeys); - }); - }, - - render() { - if (!this._suppressUpdate) { - WorkspaceHeaderView.prototype.render.apply(this, arguments); - } - }, - - toggleSelection() { - this._suppressUpdate = true; - const selectedCount = this.options.app.list.where({ selected: true }).length; - const someSelected = selectedCount > 0; - return someSelected ? this.selectNone() : this.selectAll(); - }, - - selectNone() { - this.options.app.list.where({ selected: true }).forEach(issue => { - issue.set({ selected: false }); - }); - this._suppressUpdate = false; - this.render(); - }, - - selectAll() { - this.options.app.list.forEach(issue => { - issue.set({ selected: true }); - }); - this._suppressUpdate = false; - this.render(); - }, - - returnToList() { - this.options.app.controller.closeComponentViewer(); - }, - - newSearch() { - this.options.app.controller.newSearch(); - }, - - bulkChange() { - const query = this.options.app.controller.getQueryAsObject(); - new BulkChangeForm({ - query, - context: this.context, - onChange: () => this.afterBulkChange() - }).render(); - }, - - bulkChangeSelected() { - const selected = this.options.app.list.where({ selected: true }); - const selectedKeys = selected.map(item => item.id).slice(0, 500); - const query = { issues: selectedKeys.join() }; - new BulkChangeForm({ - query, - context: this.context, - onChange: () => this.afterBulkChange() - }).render(); - }, - - serializeData() { - const issuesCount = this.options.app.list.length; - const selectedCount = this.options.app.list.where({ selected: true }).length; - const allSelected = issuesCount > 0 && issuesCount === selectedCount; - const someSelected = !allSelected && selectedCount > 0; - const data = { - ...WorkspaceHeaderView.prototype.serializeData.apply(this, arguments), - selectedCount, - allSelected, - someSelected - }; - const component = this.options.app.state.get('component'); - if (component) { - const qualifier = this.options.app.state.get('contextComponentQualifier'); - if (qualifier === 'VW' || qualifier === 'SVW') { - // do nothing - } else if (qualifier === 'TRK') { - data.state.component.project = null; - } else if (qualifier === 'BRC') { - data.state.component.project = null; - data.state.component.subProject = null; - } else { - const organization = areThereCustomOrganizations() - ? getOrganization(component.projectOrganization) - : null; - Object.assign(data, { organization }); - } - } - return data; - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js deleted file mode 100644 index ec59af550c0..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import ConnectedIssue from '../../components/issue/ConnectedIssue'; -import IssueFilterView from './issue-filter-view'; -import WithStore from '../../components/shared/WithStore'; -import getStore from '../../app/utils/getStore'; -import { getIssueByKey } from '../../store/rootReducer'; - -const SHOULD_NULL = { - any: ['issues'], - resolutions: ['resolved'], - resolved: ['resolutions'], - assignees: ['assigned'], - assigned: ['assignees'] -}; - -export default Marionette.ItemView.extend({ - className: 'issues-workspace-list-item', - - initialize(options) { - this.openComponentViewer = this.openComponentViewer.bind(this); - this.onIssueFilterClick = this.onIssueFilterClick.bind(this); - this.onIssueCheck = this.onIssueCheck.bind(this); - this.listenTo(options.app.state, 'change:selectedIndex', this.showIssue); - this.listenTo(this.model, 'change:selected', this.showIssue); - this.subscribeToStore(); - }, - - template() { - return '<div></div>'; - }, - - subscribeToStore() { - const store = getStore(); - store.subscribe(() => { - const issue = getIssueByKey(store.getState(), this.model.get('key')); - this.model.set(issue); - }); - }, - - onRender() { - this.showIssue(); - }, - - onDestroy() { - unmountComponentAtNode(this.el); - }, - - showIssue() { - const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - - render( - <WithStore> - <ConnectedIssue - issueKey={this.model.get('key')} - checked={this.model.get('selected')} - onCheck={this.onIssueCheck} - onClick={this.openComponentViewer} - onFilterClick={this.onIssueFilterClick} - selected={selected} - /> - </WithStore>, - this.el - ); - }, - - onIssueFilterClick(e) { - const that = this; - e.preventDefault(); - e.stopPropagation(); - $('body').click(); - this.popup = new IssueFilterView({ - triggerEl: $(e.currentTarget), - bottomRight: true, - model: this.model - }); - this.popup.on('select', (property, value) => { - const obj = {}; - obj[property] = '' + value; - SHOULD_NULL.any.forEach(p => { - obj[p] = null; - }); - if (SHOULD_NULL[property] != null) { - SHOULD_NULL[property].forEach(p => { - obj[p] = null; - }); - } - that.options.app.state.updateFilter(obj); - that.popup.destroy(); - }); - this.popup.render(); - }, - - onIssueCheck(e) { - e.preventDefault(); - e.stopPropagation(); - this.model.set({ selected: !this.model.get('selected') }); - const selected = this.model.collection.where({ selected: true }).length; - this.options.app.state.set({ selected }); - }, - - changeSelection() { - const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - if (selected) { - this.select(); - } else { - this.unselect(); - } - }, - - selectCurrent() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - }, - - resetIssue(options) { - const that = this; - const key = this.model.get('key'); - const componentUuid = this.model.get('componentUuid'); - const index = this.model.get('index'); - const selected = this.model.get('selected'); - this.model.reset( - { - key, - componentUuid, - index, - selected - }, - { silent: true } - ); - return this.model.fetch(options).done(() => that.trigger('reset')); - }, - - openComponentViewer() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - if (this.options.app.state.has('component')) { - return this.options.app.controller.closeComponentViewer(); - } else { - return this.options.app.controller.showComponentViewer(this.model); - } - } -}); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js deleted file mode 100644 index f74b7c912b2..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import WorkspaceListView from '../../components/navigator/workspace-list-view'; -import IssueView from './workspace-list-item-view'; -import EmptyView from './workspace-list-empty-view'; -import Template from './templates/issues-workspace-list.hbs'; -import ComponentTemplate from './templates/issues-workspace-list-component.hbs'; -import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils'; - -const COMPONENT_HEIGHT = 29; -const BOTTOM_OFFSET = 60; - -export default WorkspaceListView.extend({ - template: Template, - componentTemplate: ComponentTemplate, - childView: IssueView, - childViewContainer: '.js-list', - emptyView: EmptyView, - - bindShortcuts() { - const that = this; - WorkspaceListView.prototype.bindShortcuts.apply(this, arguments); - key('right', 'list', () => { - const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); - that.options.app.controller.showComponentViewer(selectedIssue); - return false; - }); - key('space', 'list', () => { - const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); - selectedIssue.set({ selected: !selectedIssue.get('selected') }); - return false; - }); - }, - - unbindShortcuts() { - WorkspaceListView.prototype.unbindShortcuts.apply(this, arguments); - key.unbind('right', 'list'); - key.unbind('space', 'list'); - }, - - scrollTo() { - const selectedIssue = this.collection.at(this.options.app.state.get('selectedIndex')); - if (selectedIssue == null) { - return; - } - const selectedIssueView = this.children.findByModel(selectedIssue); - const parentTopOffset = this.$el.offset().top; - let viewTop = selectedIssueView.$el.offset().top - parentTopOffset; - if (selectedIssueView.$el.prev().is('.issues-workspace-list-component')) { - viewTop -= COMPONENT_HEIGHT; - } - const viewBottom = selectedIssueView.$el.offset().top + - selectedIssueView.$el.outerHeight() + - BOTTOM_OFFSET; - const windowTop = $(window).scrollTop(); - const windowBottom = windowTop + $(window).height(); - if (viewTop < windowTop) { - $(window).scrollTop(viewTop); - } - if (viewBottom > windowBottom) { - $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom); - } - }, - - attachHtml(compositeView, childView, index) { - const container = this.getChildViewContainer(compositeView); - const model = this.collection.at(index); - if (model != null) { - const prev = index > 0 && this.collection.at(index - 1); - let putComponent = !prev; - if (prev) { - const fullComponent = [model.get('project'), model.get('component')].join(' '); - const fullPrevComponent = [prev.get('project'), prev.get('component')].join(' '); - if (fullComponent !== fullPrevComponent) { - putComponent = true; - } - } - if (putComponent) { - this.displayComponent(container, model); - } - } - container.append(childView.el); - }, - - displayComponent(container, model) { - const data = { ...model.toJSON() }; - const qualifier = this.options.app.state.get('contextComponentQualifier'); - if (qualifier === 'VW' || qualifier === 'SVW') { - Object.assign(data, { organization: undefined }); - } else if (qualifier === 'TRK') { - Object.assign(data, { organization: undefined, project: undefined }); - } else if (qualifier === 'BRC') { - Object.assign(data, { organization: undefined, project: undefined, subProject: undefined }); - } else { - const organization = areThereCustomOrganizations() - ? getOrganization(model.get('projectOrganization')) - : null; - Object.assign(data, { organization }); - } - container.append(this.componentTemplate(data)); - }, - - destroyChildren() { - WorkspaceListView.prototype.destroyChildren.apply(this, arguments); - this.$('.issues-workspace-list-component').remove(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js index bf1b7d89862..1fba6f72119 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js @@ -51,7 +51,7 @@ class OrganizationFavoriteProjects extends React.Component { render() { return ( - <div id="projects-page" className="page page-limited"> + <div id="projects-page"> <Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" /> <FavoriteProjectsContainer location={this.props.location} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js index 77eabf062da..67dbdf8bcd4 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js @@ -51,7 +51,7 @@ class OrganizationProjects extends React.Component { render() { return ( - <div id="projects-page" className="page page-limited"> + <div id="projects-page"> <Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" /> <AllProjectsContainer isFavorite={false} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap index d04badd1faf..b29c17b3a5c 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap @@ -47,7 +47,18 @@ exports[`test new_reliability_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "sinceLeakPeriod": "true", + "types": "BUG", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div @@ -91,7 +102,18 @@ exports[`test new_security_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "sinceLeakPeriod": "true", + "types": "VULNERABILITY", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div @@ -135,7 +157,17 @@ exports[`test new_sqale_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL|sinceLeakPeriod=true"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "sinceLeakPeriod": "true", + "types": "CODE_SMELL", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div @@ -223,7 +255,17 @@ exports[`test reliability_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "types": "BUG", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div @@ -267,7 +309,17 @@ exports[`test security_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", + "types": "VULNERABILITY", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div @@ -311,7 +363,16 @@ exports[`test sqale_rating 1`] = ` className="overview-quality-gate-condition overview-quality-gate-condition-error" onlyActiveOnIndex={false} style={Object {}} - to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL"> + to={ + Object { + "pathname": "/project/issues", + "query": Object { + "id": "abcd-key", + "resolved": "false", + "types": "CODE_SMELL", + }, + } + }> <div className="overview-quality-gate-condition-container"> <div diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js index 2d97ab9b996..9fd4ffc417e 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js @@ -22,7 +22,7 @@ import { Link } from 'react-router'; import { difference } from 'lodash'; import Backbone from 'backbone'; import { PermissionTemplateType, CallbackType } from '../propTypes'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; import UpdateView from '../views/UpdateView'; import DeleteView from '../views/DeleteView'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js index 7928cb05f4c..bb722731c4f 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js +++ b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js @@ -19,7 +19,7 @@ */ import React from 'react'; import UpdateKeyForm from './UpdateKeyForm'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; export default class FineGrainedUpdate extends React.Component { render() { diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projects-admin/projects.js index 88c0940368d..f3984b0cbdd 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/projects.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/projects.js @@ -23,7 +23,7 @@ import { Link } from 'react-router'; import { getComponentPermissionsUrl } from '../../helpers/urls'; import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView'; import Checkbox from '../../components/controls/Checkbox'; -import QualifierIcon from '../../components/shared/qualifier-icon'; +import QualifierIcon from '../../components/shared/QualifierIcon'; import { translate } from '../../helpers/l10n'; export default class Projects extends React.Component { diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index ec5299b31a8..c62d9e8c612 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -24,6 +24,11 @@ import ProjectsListFooterContainer from './ProjectsListFooterContainer'; import PageSidebar from './PageSidebar'; import VisualizationsContainer from '../visualizations/VisualizationsContainer'; import { parseUrlQuery } from '../store/utils'; +import Page from '../../../components/layout/Page'; +import PageMain from '../../../components/layout/PageMain'; +import PageMainInner from '../../../components/layout/PageMainInner'; +import PageSide from '../../../components/layout/PageSide'; +import PageFilters from '../../../components/layout/PageFilters'; import '../styles.css'; export default class AllProjects extends React.Component { @@ -95,38 +100,41 @@ export default class AllProjects extends React.Component { const top = this.props.organization ? 95 : 30; return ( - <div className="page-with-sidebar page-with-left-sidebar projects-page"> - <aside className="page-sidebar-fixed page-sidebar-sticky projects-sidebar"> - <div className="page-sidebar-sticky-inner" style={{ top }}> + <Page className="projects-page"> + <PageSide top={top}> + <PageFilters> <PageSidebar query={query} isFavorite={this.props.isFavorite} organization={this.props.organization} /> - </div> - </aside> - <div className="page-main"> - <PageHeaderContainer onViewChange={this.handleViewChange} view={view} /> - {view === 'list' && - <ProjectsListContainer - isFavorite={this.props.isFavorite} - isFiltered={isFiltered} - organization={this.props.organization} - />} - {view === 'list' && - <ProjectsListFooterContainer - query={query} - isFavorite={this.props.isFavorite} - organization={this.props.organization} - />} - {view === 'visualizations' && - <VisualizationsContainer - onVisualizationChange={this.handleVisualizationChange} - sort={query.sort} - visualization={visualization} - />} - </div> - </div> + </PageFilters> + </PageSide> + + <PageMain> + <PageMainInner> + <PageHeaderContainer onViewChange={this.handleViewChange} view={view} /> + {view === 'list' && + <ProjectsListContainer + isFavorite={this.props.isFavorite} + isFiltered={isFiltered} + organization={this.props.organization} + />} + {view === 'list' && + <ProjectsListFooterContainer + query={query} + isFavorite={this.props.isFavorite} + organization={this.props.organization} + />} + {view === 'visualizations' && + <VisualizationsContainer + onVisualizationChange={this.handleVisualizationChange} + sort={query.sort} + visualization={visualization} + />} + </PageMainInner> + </PageMain> + </Page> ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js index c5c3b94bda1..69c18380bb3 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/App.js +++ b/server/sonar-web/src/main/js/apps/projects/components/App.js @@ -32,7 +32,7 @@ export default class App extends React.Component { render() { return ( - <div id="projects-page" className="page page-limited"> + <div id="projects-page"> <Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" /> {this.props.children} </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js index ed924559a85..d102a9dcef4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -55,14 +55,14 @@ export default class PageSidebar extends React.PureComponent { : undefined; return ( - <div className="search-navigator-facets-list"> + <div> <FavoriteFilterContainer organization={this.props.organization} /> <div className="projects-facets-header clearfix"> {isFiltered && <div className="projects-facets-reset"> <Link to={{ pathname, query: linkQuery }} className="button button-red"> - {translate('projects.clear_all_filters')} + {translate('clear_all_filters')} </Link> </div>} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js index 1c4cae090f2..d692e2ca9ce 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js @@ -19,9 +19,9 @@ */ import React from 'react'; import ProjectCardContainer from './ProjectCardContainer'; -import NoProjects from './NoProjects'; import NoFavoriteProjects from './NoFavoriteProjects'; import EmptyInstance from './EmptyInstance'; +import EmptySearch from '../../../components/common/EmptySearch'; export default class ProjectsList extends React.PureComponent { static propTypes = { @@ -37,7 +37,7 @@ export default class ProjectsList extends React.PureComponent { } else if (!this.props.isFiltered) { return <EmptyInstance />; } else { - return <NoProjects />; + return <EmptySearch />; } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap index 211b0fbafb1..faf96ac7d0a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap @@ -16,7 +16,7 @@ exports[`test should handle \`view\` and \`visualization\` 2`] = ` }, } }> - projects.clear_all_filters + clear_all_filters </Link> </div> `; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 45f8cd2663b..99e3232f7d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -10,14 +10,6 @@ margin-bottom: 0; } -.projects-empty-list { - padding: 60px 0; - border: 1px solid #e6e6e6; - border-radius: 2px; - text-align: center; - color: #777; -} - .project-card { position: relative; min-height: 121px; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js index 6d877d785fa..171d4ab8e0d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js @@ -21,7 +21,7 @@ import React from 'react'; import { Link } from 'react-router'; import ChangeProjectsView from '../views/ChangeProjectsView'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; import { getProfileProjects } from '../../../api/quality-profiles'; import { translate } from '../../../helpers/l10n'; import type { Profile } from '../propTypes'; |