Переглянути джерело

apply feedback for issues page (#1980)

tags/6.4-RC1
Stas Vilchik 7 роки тому
джерело
коміт
3e72937a66
78 змінених файлів з 1190 додано та 909 видалено
  1. 0
    1
      server/sonar-web/package.json
  2. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
  3. 15
    6
      server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs
  4. 2
    2
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js
  5. 24
    6
      server/sonar-web/src/main/js/apps/issues/actions.js
  6. 104
    63
      server/sonar-web/src/main/js/apps/issues/components/App.js
  7. 1
    6
      server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
  8. 1
    1
      server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
  9. 2
    5
      server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
  10. 9
    5
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
  11. 1
    2
      server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
  12. 7
    4
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  13. 10
    6
      server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js
  14. 10
    6
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js
  15. 25
    7
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
  16. 2
    2
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
  17. 1
    1
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
  18. 41
    23
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
  19. 26
    16
      server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
  20. 6
    7
      server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
  21. 26
    16
      server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
  22. 26
    16
      server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
  23. 35
    18
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
  24. 26
    16
      server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
  25. 42
    24
      server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
  26. 6
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
  27. 35
    19
      server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
  28. 6
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
  29. 13
    11
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
  30. 6
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
  31. 36
    19
      server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
  32. 6
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
  33. 1
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
  34. 1
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
  35. 21
    8
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
  36. 5
    11
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
  37. 41
    23
      server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
  38. 16
    12
      server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
  39. 144
    105
      server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
  40. 29
    2
      server/sonar-web/src/main/js/apps/issues/styles.css
  41. 9
    0
      server/sonar-web/src/main/js/apps/issues/utils.js
  42. 19
    20
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  43. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
  44. 2
    2
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
  45. 47
    50
      server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
  46. 10
    7
      server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js
  47. 8
    4
      server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js
  48. 8
    5
      server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js
  49. 2
    2
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js
  50. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js
  51. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js
  52. 26
    24
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap
  53. 14
    13
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap
  54. 12
    11
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap
  55. 7
    0
      server/sonar-web/src/main/js/components/common/EmptySearch.css
  56. 2
    9
      server/sonar-web/src/main/js/components/common/EmptySearch.js
  57. 12
    1
      server/sonar-web/src/main/js/components/common/SelectList.js
  58. 1
    3
      server/sonar-web/src/main/js/components/controls/Checkbox.js
  59. 31
    26
      server/sonar-web/src/main/js/components/issue/Issue.js
  60. 5
    1
      server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
  61. 4
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
  62. 2
    6
      server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
  63. 9
    1
      server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
  64. 0
    42
      server/sonar-web/src/main/js/components/layout/Page.js
  65. 0
    34
      server/sonar-web/src/main/js/components/layout/PageFilters.js
  66. 0
    34
      server/sonar-web/src/main/js/components/layout/PageMain.js
  67. 0
    34
      server/sonar-web/src/main/js/components/layout/PageMainInner.js
  68. 0
    73
      server/sonar-web/src/main/js/components/layout/PageSide.js
  69. 27
    9
      server/sonar-web/src/main/js/helpers/scrolling.js
  70. 7
    1
      server/sonar-web/src/main/js/helpers/urls.js
  71. 7
    1
      server/sonar-web/src/main/less/components/badges.less
  72. 2
    0
      server/sonar-web/src/main/less/components/issues.less
  73. 5
    0
      server/sonar-web/src/main/less/components/modals.less
  74. 63
    0
      server/sonar-web/src/main/less/components/page.less
  75. 6
    0
      server/sonar-web/src/main/less/components/search-navigator.less
  76. 21
    1
      server/sonar-web/src/main/less/pages/issues.less
  77. 7
    15
      server/sonar-web/yarn.lock
  78. 3
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 1
server/sonar-web/package.json Переглянути файл

@@ -17,7 +17,6 @@
"d3-selection": "1.0.5",
"d3-shape": "1.0.6",
"escape-html": "1.0.3",
"glamor": "2.20.24",
"handlebars": "2.0.0",
"history": "2.0.0",
"jquery": "2.2.0",

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js Переглянути файл

@@ -62,7 +62,7 @@ export default class GlobalNavMenu extends React.PureComponent {

renderIssuesLink() {
const query = this.props.currentUser.isLoggedIn
? { myIssues: 'true', resolved: 'false' }
? { createdInLast: '1w', myIssues: 'true', resolved: 'false' }
: { resolved: 'false' };
const active = this.props.location.pathname === 'issues';
return (

+ 15
- 6
server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs Переглянути файл

@@ -26,8 +26,7 @@

<h3 class="shortcuts-section-title">{{t 'shortcuts.section.rules'}}</h3>
<ul class="shortcuts-list">
<li><span class="shortcut-button">&uarr;</span> <span
class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.navigate_between_rules'}}</li>
<li><span class="shortcut-button">&uarr;</span> <span class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.navigate_between_rules'}}</li>
<li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.open_details'}}</li>
<li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.return_to_list'}}</li>
<li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.activate'}}</li>
@@ -38,12 +37,22 @@
<div class="column-half">
<h3 class="shortcuts-section-title">{{t 'shortcuts.section.issues'}}</h3>
<ul class="shortcuts-list">
<li><span class="shortcut-button">&uarr;</span> <span
class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.navigate_between_issues'}}
<li><span class="shortcut-button">&uarr;</span> <span class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.navigate_between_issues'}}
</li>
<li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.open_details'}}</li>
<li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.return_to_list'}}</li>
<li><span class="shortcut-button">⎵</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.select'}}</li>
<li>
<span class="shortcut-button">alt</span>
<span class=>+</span>
<span class="shortcut-button">↑</span>
<span class="shortcut-button">↓</span> {{t 'issues.to_navigate_issue_locations'}}
</li>
<li>
<span class="shortcut-button">alt</span>
<span class=>+</span>
<span class="shortcut-button">←</span>
<span class="shortcut-button">→</span> {{t 'issues.to_switch_flows'}}
</li>
<li><span class="shortcut-button">f</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.do_transition'}}</li>
<li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign'}}</li>
<li><span class="shortcut-button">m</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign_to_me'}}</li>
@@ -58,4 +67,4 @@

<div class="modal-foot">
<a class="js-modal-close" href="#">{{t 'close'}}</a>
</div>
</div>

+ 2
- 2
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js Переглянути файл

@@ -20,7 +20,7 @@
import $ from 'jquery';
import Marionette from 'backbone.marionette';
import Template from '../templates/rule/coding-rules-rule-issues.hbs';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { getComponentIssuesUrlAsString } from '../../../helpers/urls';

export default Marionette.ItemView.extend({
template: Template,
@@ -55,7 +55,7 @@ export default Marionette.ItemView.extend({
...project,
name: projectBase != null ? projectBase.longName : '',
issuesUrl: projectBase != null &&
getComponentIssuesUrl(projectBase.key, {
getComponentIssuesUrlAsString(projectBase.key, {
resolved: 'false',
rules: this.model.id
})

+ 24
- 6
server/sonar-web/src/main/js/apps/issues/actions.js Переглянути файл

@@ -20,12 +20,16 @@
// @flow
import type { State } from './components/App';

export const enableLocationsNavigator = (state: State) => ({
locationsNavigator: true,
selectedFlowIndex: state.selectedFlowIndex ||
(state.openIssue && state.openIssue.flows.length > 0 ? 0 : null),
selectedLocationIndex: state.selectedLocationIndex || 0
});
export const enableLocationsNavigator = (state: State) => {
const { openIssue } = state;
if (openIssue && (openIssue.secondaryLocations.length > 0 || openIssue.flows.length > 0)) {
return {
locationsNavigator: true,
selectedFlowIndex: state.selectedFlowIndex || (openIssue.flows.length > 0 ? 0 : null),
selectedLocationIndex: state.selectedLocationIndex || 0
};
}
};

export const disableLocationsNavigator = () => ({
locationsNavigator: false
@@ -70,3 +74,17 @@ export const selectPreviousLocation = (state: State) => {
export const selectFlow = (nextIndex: ?number) => () => {
return { selectedFlowIndex: nextIndex, selectedLocationIndex: 0 };
};

export const selectNextFlow = (state: State) => {
const { openIssue, selectedFlowIndex } = state;
if (openIssue && selectedFlowIndex != null && openIssue.flows.length > selectedFlowIndex + 1) {
return { selectedFlowIndex: selectedFlowIndex + 1, selectedLocationIndex: 0 };
}
};

export const selectPreviousFlow = (state: State) => {
const { openIssue, selectedFlowIndex } = state;
if (openIssue && selectedFlowIndex != null && selectedFlowIndex > 0) {
return { selectedFlowIndex: selectedFlowIndex - 1, selectedLocationIndex: 0 };
}
};

+ 104
- 63
server/sonar-web/src/main/js/apps/issues/components/App.js Переглянути файл

@@ -39,7 +39,8 @@ import {
areQueriesEqual,
getOpen,
serializeQuery,
parseFacets
parseFacets,
mapFacet
} from '../utils';
import type {
Query,
@@ -53,11 +54,6 @@ import type {
} 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';
@@ -227,6 +223,14 @@ export default class App extends React.PureComponent {
// alt + down
event.preventDefault();
this.selectPreviousLocation();
} else if (event.keyCode === 37 && event.altKey) {
// alt + left
event.preventDefault();
this.selectPreviousFlow();
} else if (event.keyCode === 39 && event.altKey) {
// alt + right
event.preventDefault();
this.selectNextFlow();
}
};

@@ -311,6 +315,7 @@ export default class App extends React.PureComponent {
open: undefined
}
});
this.scrollToSelectedIssue(false);
}
};

@@ -321,43 +326,30 @@ export default class App extends React.PureComponent {
}
};

scrollToSelectedIssue = () => {
scrollToSelectedIssue = (smooth: boolean = true) => {
const { selected } = this.state;
if (selected) {
const element = document.querySelector(`[data-issue="${selected}"]`);
if (element) {
scrollToElement(element, 150, 100);
scrollToElement(element, { topOffset: 150, bottomOffset: 100, smooth });
}
}
};

fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
const { component } = this.props;
const { myIssues, query } = this.state;
const { myIssues, openFacets, query } = this.state;

const facets = requestFacets
? Object.keys(openFacets).filter(facet => openFacets[facet]).map(mapFacet).join(',')
: undefined;

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,
ps: 100,
facets,
...additional
};

@@ -464,6 +456,32 @@ export default class App extends React.PureComponent {
});
};

fetchFacet = (facet: string) => {
return this.fetchIssues({ ps: 1, facets: mapFacet(facet) }).then(({ facets, ...other }) => {
if (this.mounted) {
this.setState(state => ({
facets: { ...state.facets, ...parseFacets(facets) },
referencedComponents: {
...state.referencedComponents,
...keyBy(other.components, 'uuid')
},
referencedLanguages: {
...state.referencedLanguages,
...keyBy(other.languages, 'key')
},
referencedRules: {
...state.referencedRules,
...keyBy(other.rules, 'key')
},
referencedUsers: {
...state.referencedUsers,
...keyBy(other.users, 'login')
}
}));
}
});
};

isFiltered = () => {
const serialized = serializeQuery(this.state.query);
return !areQueriesEqual(serialized, DEFAULT_QUERY);
@@ -510,6 +528,9 @@ export default class App extends React.PureComponent {
this.setState(state => ({
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
}));
if (!this.state.facets[property]) {
this.fetchFacet(property);
}
};

handleReset = () => {
@@ -564,6 +585,10 @@ export default class App extends React.PureComponent {
this.closeBulkChange();
};

handleReload = () => {
this.fetchFirstIssues();
};

handleReloadAndOpenFirst = () => {
this.fetchFirstIssues().then(issues => {
if (issues.length > 0) {
@@ -576,6 +601,8 @@ export default class App extends React.PureComponent {
selectNextLocation = () => this.setState(actions.selectNextLocation);
selectPreviousLocation = () => this.setState(actions.selectPreviousLocation);
selectFlow = (index: ?number) => this.setState(actions.selectFlow(index));
selectNextFlow = () => this.setState(actions.selectNextFlow);
selectPreviousFlow = () => this.setState(actions.selectPreviousFlow);

renderBulkChange(openIssue: ?Issue) {
const { component, currentUser } = this.props;
@@ -627,7 +654,7 @@ export default class App extends React.PureComponent {
const { query } = this.state;

return (
<PageFilters>
<div className="layout-page-filters">
{currentUser.isLoggedIn &&
<MyIssuesFilter
myIssues={this.state.myIssues}
@@ -647,7 +674,7 @@ export default class App extends React.PureComponent {
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
</PageFilters>
</div>
);
}

@@ -655,7 +682,7 @@ export default class App extends React.PureComponent {
const { issues, paging } = this.state;

return (
<PageFilters>
<div className="layout-page-filters">
<ConciseIssuesListHeader
loading={this.state.loading}
onBackClick={this.closeIssue}
@@ -675,7 +702,7 @@ export default class App extends React.PureComponent {
{paging != null &&
paging.total > 0 &&
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
</PageFilters>
</div>
);
}

@@ -683,24 +710,28 @@ export default class App extends React.PureComponent {
const top = this.props.component ? 95 : 30;

return (
<PageSide top={top}>
{openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()}
</PageSide>
<div className="layout-page-side-outer">
<div className="layout-page-side" style={{ top }}>
<div className="layout-page-side-inner">
{openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()}
</div>
</div>
</div>
);
}

renderList(openIssue: ?Issue) {
renderList() {
const { component, currentUser } = this.props;
const { issues, paging } = this.state;
const { issues, openIssue, paging } = this.state;
const selectedIndex = this.getSelectedIndex();
const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;

if (paging == null) {
if (paging == null || openIssue != null) {
return null;
}

return (
<div className={openIssue != null ? 'hidden' : undefined}>
<div>
{paging.total > 0 &&
<IssuesList
checked={this.state.checked}
@@ -722,12 +753,22 @@ export default class App extends React.PureComponent {
}

renderShortcutsForLocations() {
const { openIssue } = this.state;
if (openIssue == null || (!openIssue.secondaryLocations.length && !openIssue.flows.length)) {
return null;
}
const hasSeveralFlows = openIssue.flows.length > 1;
return (
<div className="pull-right note">
<span className="shortcut-button little-spacer-right">alt</span>
<span className="little-spacer-right">{'+'}</span>
<span className="shortcut-button little-spacer-right">↑</span>
<span className="shortcut-button little-spacer-right">↓</span>
{hasSeveralFlows &&
<span>
<span className="shortcut-button little-spacer-right">←</span>
<span className="shortcut-button little-spacer-right">→</span>
</span>}
{translate('issues.to_navigate_issue_locations')}
</div>
);
@@ -740,50 +781,50 @@ export default class App extends React.PureComponent {
const selectedIndex = this.getSelectedIndex();

return (
<Page className="issues" id="issues-page">
<div className="layout-page issues" id="issues-page">
<Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />

{this.renderSide(openIssue)}

<PageMain>
<div className="layout-page-main">
<div className="issues-header-panel issues-main-header">
<div className="issues-header-panel-inner issues-main-header-inner">
<PageMainInner>
<div className="layout-page-main-inner">
{this.renderBulkChange(openIssue)}
{openIssue != null
? <div className="pull-left">
? <div className="pull-left width-60">
<ComponentBreadcrumbs component={component} issue={openIssue} />
</div>
: <PageActions
loading={this.state.loading}
onReload={this.handleReload}
paging={paging}
selectedIndex={selectedIndex}
/>}
{openIssue != null && this.renderShortcutsForLocations()}
</PageMainInner>
{this.renderShortcutsForLocations()}
</div>
</div>
</div>

<PageMainInner>
<div className="layout-page-main-inner">
<div>
{openIssue != null &&
<IssuesSourceViewer
openIssue={openIssue}
loadIssues={this.fetchIssuesForComponent}
onIssueChange={this.handleIssueChange}
onIssueSelect={this.openIssue}
onLocationSelect={this.selectLocation}
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={
this.state.locationsNavigator ? this.state.selectedLocationIndex : null
}
/>}

{this.renderList(openIssue)}
{openIssue
? <IssuesSourceViewer
openIssue={openIssue}
loadIssues={this.fetchIssuesForComponent}
onIssueChange={this.handleIssueChange}
onIssueSelect={this.openIssue}
onLocationSelect={this.selectLocation}
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={
this.state.locationsNavigator ? this.state.selectedLocationIndex : null
}
/>
: this.renderList()}
</div>
</PageMainInner>
</PageMain>
</Page>
</div>
</div>
</div>
);
}
}

+ 1
- 6
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js Переглянути файл

@@ -21,7 +21,6 @@
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';
@@ -228,11 +227,7 @@ export default class BulkChangeModal extends React.PureComponent {
);

renderCheckbox = (field: string) => (
<Checkbox
className={css({ paddingTop: 6, paddingRight: 8 })}
checked={this.state[field] != null}
onCheck={this.handleFieldCheck(field)}
/>
<Checkbox checked={this.state[field] != null} onCheck={this.handleFieldCheck(field)} />
);

renderAffected = (affected: number) => (

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js Переглянути файл

@@ -42,7 +42,7 @@ export default class ComponentBreadcrumbs extends React.PureComponent {
const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier);

return (
<div className="component-name">
<div className="component-name text-ellipsis">
{displayOrganization &&
<Organization linkClassName="link-no-underline" organizationKey={issue.organization} />}


+ 2
- 5
server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js Переглянути файл

@@ -19,7 +19,6 @@
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';

type Props = {
@@ -27,8 +26,6 @@ type Props = {
onReset: () => void
};

const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });

export default class FiltersHeader extends React.PureComponent {
props: Props;

@@ -40,9 +37,9 @@ export default class FiltersHeader extends React.PureComponent {

render() {
return (
<div className={styles}>
<div className="issues-filters-header">
{this.props.displayReset &&
<div className={css({ float: 'right' })}>
<div className="pull-right">
<button className="button-red" onClick={this.handleResetClick}>
{translate('clear_all_filters')}
</button>

+ 9
- 5
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js Переглянути файл

@@ -46,16 +46,20 @@ export default class IssuesSourceViewer extends React.PureComponent {
}
}

scrollToIssue = () => {
scrollToIssue = (smooth: boolean = true) => {
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
if (element) {
this.handleScroll(element);
this.handleScroll(element, smooth);
}
};

handleScroll = (element: HTMLElement) => {
handleScroll = (element: HTMLElement, smooth: boolean = true) => {
const offset = window.innerHeight / 2;
scrollToElement(element, offset - 100, offset);
scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth });
};

handleLoaded = () => {
this.scrollToIssue(false);
};

render() {
@@ -80,7 +84,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
highlightedLocations={locations}
highlightedLocationMessage={locationMessage}
loadIssues={this.props.loadIssues}
onLoaded={this.scrollToIssue}
onLoaded={this.handleLoaded}
onLocationSelect={this.props.onLocationSelect}
onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}

+ 1
- 2
server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js Переглянути файл

@@ -19,7 +19,6 @@
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';

type Props = {|
@@ -40,7 +39,7 @@ export default class MyIssuesFilter extends React.PureComponent {
const { myIssues } = this.props;

return (
<div className={css({ marginBottom: 24, textAlign: 'center' })}>
<div className="issues-my-issues-filter">
<div className="button-group">
<button
className={myIssues ? 'button-active' : undefined}

+ 7
- 4
server/sonar-web/src/main/js/apps/issues/components/PageActions.js Переглянути файл

@@ -19,13 +19,14 @@
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import IssuesCounter from './IssuesCounter';
import ReloadButton from './ReloadButton';
import type { Paging } from '../utils';
import { translate } from '../../../helpers/l10n';

type Props = {|
loading: boolean,
onReload: () => void,
paging: ?Paging,
selectedIndex: ?number
|};
@@ -55,11 +56,13 @@ export default class PageActions extends React.PureComponent {
const { paging, selectedIndex } = this.props;

return (
<div className={css({ float: 'right' })}>
<div className="pull-right">
{this.renderShortcuts()}

<div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
{this.props.loading && <i className="spinner spacer-right" />}
<div className="issues-page-actions">
{this.props.loading
? <i className="issues-main-header-spinner spinner" />
: <ReloadButton className="spacer-right" onClick={this.props.onReload} />}
{paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
</div>
</div>

server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js → server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js Переглянути файл

@@ -20,6 +20,8 @@
// @flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';

type Props = {|
className?: string,
@@ -41,11 +43,13 @@ export default function ReloadButton(props: Props) {
};

return (
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
<Tooltip overlay={translate('reload')}>
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
</Tooltip>
);
}

+ 10
- 6
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js Переглянути файл

@@ -20,6 +20,8 @@
// @flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';

type Props = {|
className?: string,
@@ -41,11 +43,13 @@ export default function BackButton(props: Props) {
};

return (
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
<Tooltip overlay={translate('issues.return_to_list')}>
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
</Tooltip>
);
}

+ 25
- 7
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js Переглянути файл

@@ -31,30 +31,47 @@ type Props = {|
onClick: string => void,
onFlowSelect: number => void,
onLocationSelect: number => void,
scroll: HTMLElement => void,
scroll: (element: HTMLElement, bottomOffset: ?number) => void,
selected: boolean,
selectedFlowIndex: ?number,
selectedLocationIndex: ?number
|};

export default class ConciseIssueBox extends React.PureComponent {
node: HTMLElement;
messageElement: HTMLElement;
rootElement: HTMLElement;
props: Props;

componentDidMount() {
// scroll to the message element and not to the root element,
// because the root element can be huge and exceed the window height
if (this.props.selected) {
this.props.scroll(this.node);
this.handleScroll();
}
}

componentDidUpdate(prevProps: Props) {
if (this.props.selected && prevProps.selected !== this.props.selected) {
this.props.scroll(this.node);
this.handleScroll();
}
}

handleScroll = () => {
const { selectedFlowIndex } = this.props;
const { flows, secondaryLocations } = this.props.issue;

const locations = selectedFlowIndex != null
? flows[selectedFlowIndex]
: flows.length > 0 ? flows[0] : secondaryLocations;

if (locations == null || locations.length < 15) {
// if there are no locations, or there are just few
// then ensuse that the whole box is visible
this.props.scroll(this.rootElement);
} else {
// otherwise scroll until the the message element is located on top
this.props.scroll(this.messageElement, window.innerHeight - 200);
}
};

handleClick = (event: Event) => {
event.preventDefault();
this.props.onClick(this.props.issue.key);
@@ -70,8 +87,9 @@ export default class ConciseIssueBox extends React.PureComponent {
return (
<div
className={classNames('concise-issue-box', 'clearfix', { selected })}
ref={node => (this.rootElement = node)}
{...clickAttributes}>
<div className="concise-issue-box-message" ref={node => (this.node = node)}>
<div className="concise-issue-box-message" ref={node => (this.messageElement = node)}>
{issue.message}
</div>
<div className="concise-issue-box-attributes">

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js Переглянути файл

@@ -36,10 +36,10 @@ type Props = {|
export default class ConciseIssuesList extends React.PureComponent {
props: Props;

handleScroll = (element: HTMLElement) => {
handleScroll = (element: HTMLElement, bottomOffset: number = 100) => {
const scrollableElement = document.querySelector('.layout-page-side');
if (element && scrollableElement) {
scrollToElement(element, 150, 100, scrollableElement);
scrollToElement(element, { topOffset: 150, bottomOffset, parent: scrollableElement });
}
};


+ 1
- 1
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js Переглянути файл

@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import BackButton from './BackButton';
import ReloadButton from './ReloadButton';
import ReloadButton from '../components/ReloadButton';
import IssuesCounter from '../components/IssuesCounter';
import type { Paging } from '../utils';


+ 41
- 23
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js Переглянути файл

@@ -69,6 +69,10 @@ export default class AssigneeFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ assigned: true, assignees: [] });
};

handleSearch = (query: string) => searchAssignees(query, this.props.component);

handleSelect = (assignee: string) => {
@@ -117,7 +121,7 @@ export default class AssigneeFacet extends React.PureComponent {
);
};

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -132,36 +136,50 @@ export default class AssigneeFacet extends React.PureComponent {
key => -stats[key]
);

return (
<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>
);
}

renderFooter() {
if (!this.props.stats) {
return null;
}

return (
<FacetFooter
onSearch={this.handleSearch}
onSelect={this.handleSelect}
renderOption={this.renderOption}
/>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={!this.props.assigned || this.props.assignees.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.assignees.length + (this.props.assigned ? 0 : 1)}
/>

{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}
/>}
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
</FacetBox>
);
}

+ 26
- 16
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js Переглянути файл

@@ -56,12 +56,16 @@ export default class AuthorFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(author: string): ?number {
const { stats } = this.props;
return stats ? stats[author] : null;
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -70,29 +74,35 @@ export default class AuthorFacet extends React.PureComponent {

const authors = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.authors.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.authors.length}
/>

{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>}
{this.props.open && this.renderList()}
</FacetBox>
);
}

+ 6
- 7
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js Переглянути файл

@@ -59,6 +59,10 @@ export default class CreationDateFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.resetTo({});
};

resetTo = (changes: {}) => {
this.props.onChange({
createdAfter: undefined,
@@ -252,19 +256,14 @@ export default class CreationDateFacet extends React.PureComponent {
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)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={hasValue ? 1 : 0}
/>

{this.props.open && this.renderInner()}

+ 26
- 16
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js Переглянути файл

@@ -61,6 +61,10 @@ export default class DirectoryFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(directory: string): ?number {
const { stats } = this.props;
return stats ? stats[directory] : null;
@@ -82,7 +86,7 @@ export default class DirectoryFacet extends React.PureComponent {
);
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -91,29 +95,35 @@ export default class DirectoryFacet extends React.PureComponent {

const directories = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.directories.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.directories.length}
/>

{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>}
{this.props.open && this.renderList()}
</FacetBox>
);
}

+ 26
- 16
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js Переглянути файл

@@ -60,6 +60,10 @@ export default class FileFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(file: string): ?number {
const { stats } = this.props;
return stats ? stats[file] : null;
@@ -78,7 +82,7 @@ export default class FileFacet extends React.PureComponent {
);
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -87,29 +91,35 @@ export default class FileFacet extends React.PureComponent {

const files = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.files.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.files.length}
/>

{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>}
{this.props.open && this.renderList()}
</FacetBox>
);
}

+ 35
- 18
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js Переглянути файл

@@ -59,6 +59,10 @@ export default class LanguageFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getLanguageName(language: string): string {
const { referencedLanguages } = this.props;
return referencedLanguages[language] ? referencedLanguages[language].name : language;
@@ -74,7 +78,7 @@ export default class LanguageFacet extends React.PureComponent {
this.props.onChange({ [this.property]: uniq([...languages, language]) });
};

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -83,31 +87,44 @@ export default class LanguageFacet extends React.PureComponent {

const languages = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

renderFooter() {
if (!this.props.stats) {
return null;
}

return <LanguageFacetFooter onSelect={this.handleSelect} />;
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.languages.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.languages.length}
/>

{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} />}
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
</FacetBox>
);
}

+ 26
- 16
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js Переглянути файл

@@ -59,6 +59,10 @@ export default class ModuleFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(module: string): ?number {
const { stats } = this.props;
return stats ? stats[module] : null;
@@ -75,7 +79,7 @@ export default class ModuleFacet extends React.PureComponent {
);
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -84,29 +88,35 @@ export default class ModuleFacet extends React.PureComponent {

const modules = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.modules.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.modules.length}
/>

{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>}
{this.props.open && this.renderList()}
</FacetBox>
);
}

+ 42
- 24
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js Переглянути файл

@@ -63,6 +63,10 @@ export default class ProjectFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

handleSearch = (query: string) => {
const { component } = this.props;

@@ -116,7 +120,7 @@ export default class ProjectFacet extends React.PureComponent {
);
};

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -125,37 +129,51 @@ export default class ProjectFacet extends React.PureComponent {

const projects = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

renderFooter() {
if (!this.props.stats) {
return null;
}

return (
<FacetFooter
minimumQueryLength={3}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
renderOption={this.renderOption}
/>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.projects.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.projects.length}
/>

{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}
/>}
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
</FacetBox>
);
}

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js Переглянути файл

@@ -65,6 +65,10 @@ export default class ResolutionFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ resolved: false, resolutions: [] });
};

isFacetItemActive(resolution: string) {
return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution);
}
@@ -103,10 +107,11 @@ export default class ResolutionFacet extends React.PureComponent {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={!this.props.resolved || this.props.resolutions.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.resolutions.length}
/>

{this.props.open &&

+ 35
- 19
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js Переглянути файл

@@ -60,6 +60,10 @@ export default class RuleFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

handleSearch = (query: string) => {
const { languages } = this.props;
return searchRules({
@@ -86,7 +90,7 @@ export default class RuleFacet extends React.PureComponent {
return stats ? stats[rule] : null;
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -95,32 +99,44 @@ export default class RuleFacet extends React.PureComponent {

const rules = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

renderFooter() {
if (!this.props.stats) {
return null;
}

return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.rules.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.rules.length}
/>

{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} />}
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
</FacetBox>
);
}

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js Переглянути файл

@@ -57,6 +57,10 @@ export default class SeverityFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(severity: string): ?number {
const { stats } = this.props;
return stats ? stats[severity] : null;
@@ -87,10 +91,11 @@ export default class SeverityFacet extends React.PureComponent {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.severities.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.severities.length}
/>

{this.props.open &&

+ 13
- 11
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js Переглянути файл

@@ -65,8 +65,9 @@ export default class Sidebar extends React.PureComponent {

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 displayModulesFacet = component != null && component.qualifier !== 'DIR';
const displayDirectoriesFacet = component != null && component.qualifier !== 'DIR';
const displayFilesFacet = component != null;
const displayAuthorFacet = component == null || component.qualifier !== 'DEV';

return (
@@ -167,15 +168,16 @@ export default class Sidebar extends React.PureComponent {
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}
/>
{displayFilesFacet &&
<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}

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js Переглянути файл

@@ -56,6 +56,10 @@ export default class StatusFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(status: string): ?number {
const { stats } = this.props;
return stats ? stats[status] : null;
@@ -96,10 +100,11 @@ export default class StatusFacet extends React.PureComponent {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.statuses.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.statuses.length}
/>

{this.props.open &&

+ 36
- 19
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js Переглянути файл

@@ -58,6 +58,10 @@ export default class TagFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

handleSearch = (query: string) => {
return searchIssueTags({ ps: 50, q: query }).then(tags =>
tags.map(tag => ({ label: tag, value: tag }))
@@ -83,7 +87,7 @@ export default class TagFacet extends React.PureComponent {
);
}

render() {
renderList() {
const { stats } = this.props;

if (!stats) {
@@ -92,32 +96,45 @@ export default class TagFacet extends React.PureComponent {

const tags = sortBy(Object.keys(stats), key => -stats[key]);

return (
<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>
);
}

renderFooter() {
if (!this.props.stats) {
return null;
}

return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.tags.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.tags.length}
/>

{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} />}
{this.props.open && this.renderList()}

{this.props.open && this.renderFooter()}
</FacetBox>
);
}

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js Переглянути файл

@@ -57,6 +57,10 @@ export default class TypeFacet extends React.PureComponent {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(type: string): ?number {
const { stats } = this.props;
return stats ? stats[type] : null;
@@ -86,10 +90,11 @@ export default class TypeFacet extends React.PureComponent {
return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.types.length > 0}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.props.types.length}
/>

{this.props.open &&

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js Переглянути файл

@@ -42,7 +42,7 @@ it('should render', () => {
expect(renderAssigneeFacet()).toMatchSnapshot();
});

it('should not render without stats', () => {
it('should render without stats', () => {
expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot();
});


+ 1
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js Переглянути файл

@@ -28,7 +28,7 @@ const renderSidebar = props =>
.children()
.map(node => node.name());

it('should render all facets', () => {
it('should render facets for global page', () => {
expect(renderSidebar()).toMatchSnapshot();
});


+ 21
- 8
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap Переглянути файл

@@ -1,13 +1,12 @@
exports[`test should not render without stats 1`] = `null`;

exports[`test should render 1`] = `
<FacetBox
property="assignees">
<FacetHeader
hasValue={false}
name="issues.facet.assignees"
onClear={[Function]}
onClick={[Function]}
open={true} />
open={true}
values={0} />
<FacetItemsList>
<FacetItem
active={false}
@@ -62,14 +61,27 @@ exports[`test should render footer select option 1`] = `
</span>
`;

exports[`test should render without stats 1`] = `
<FacetBox
property="assignees">
<FacetHeader
name="issues.facet.assignees"
onClear={[Function]}
onClick={[Function]}
open={true}
values={0} />
</FacetBox>
`;

exports[`test should select unassigned 1`] = `
<FacetBox
property="assignees">
<FacetHeader
hasValue={true}
name="issues.facet.assignees"
onClear={[Function]}
onClick={[Function]}
open={true} />
open={true}
values={1} />
<FacetItemsList>
<FacetItem
active={true}
@@ -118,10 +130,11 @@ exports[`test should select user 1`] = `
<FacetBox
property="assignees">
<FacetHeader
hasValue={true}
name="issues.facet.assignees"
onClear={[Function]}
onClick={[Function]}
open={true} />
open={true}
values={1} />
<FacetItemsList>
<FacetItem
active={false}

+ 5
- 11
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap Переглянути файл

@@ -1,4 +1,4 @@
exports[`test should render all facets 1`] = `
exports[`test should render facets for developer 1`] = `
Array [
"FacetMode",
"TypeFacet",
@@ -13,12 +13,11 @@ Array [
"DirectoryFacet",
"FileFacet",
"AssigneeFacet",
"AuthorFacet",
"LanguageFacet",
]
`;

exports[`test should render facets for developer 1`] = `
exports[`test should render facets for directory 1`] = `
Array [
"FacetMode",
"TypeFacet",
@@ -28,16 +27,14 @@ Array [
"CreationDateFacet",
"RuleFacet",
"TagFacet",
"ProjectFacet",
"ModuleFacet",
"DirectoryFacet",
"FileFacet",
"AssigneeFacet",
"AuthorFacet",
"LanguageFacet",
]
`;

exports[`test should render facets for directory 1`] = `
exports[`test should render facets for global page 1`] = `
Array [
"FacetMode",
"TypeFacet",
@@ -47,7 +44,7 @@ Array [
"CreationDateFacet",
"RuleFacet",
"TagFacet",
"FileFacet",
"ProjectFacet",
"AssigneeFacet",
"AuthorFacet",
"LanguageFacet",
@@ -103,9 +100,6 @@ Array [
"RuleFacet",
"TagFacet",
"ProjectFacet",
"ModuleFacet",
"DirectoryFacet",
"FileFacet",
"AuthorFacet",
"LanguageFacet",
]

+ 41
- 23
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js Переглянути файл

@@ -20,25 +20,34 @@
// @flow
/* eslint-disable max-len */
import React from 'react';
import { translate } from '../../../../helpers/l10n';

type Props = {
hasValue: boolean,
type Props = {|
name: string,
onClear?: () => void,
onClick?: () => void,
open: boolean
};
open: boolean,
values?: number
|};

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();
handleClearClick = (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
event.currentTarget.blur();
if (this.props.onClear) {
this.props.onClear();
}
};

handleClick = (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
event.currentTarget.blur();
if (this.props.onClick) {
this.props.onClick();
}
@@ -61,23 +70,32 @@ export default class FacetHeader extends React.PureComponent {
}

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;
if (this.props.open || !this.props.values) {
return null;
}
return <span className="spacer-left badge is-rounded">{this.props.values}</span>;
}

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>;
const showClearButton: boolean = !!this.props.values && this.props.onClear != null;

return (
<div>
{showClearButton &&
<button
className="search-navigator-facet-header-button button-small button-red"
onClick={this.handleClearClick}>
{translate('clear')}
</button>}

{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>}
</div>
);
}
}

+ 16
- 12
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js Переглянути файл

@@ -25,37 +25,41 @@ import FacetHeader from '../FacetHeader';

it('should render open facet with value', () => {
expect(
shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />)
shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={1} />)
).toMatchSnapshot();
});

it('should render open facet without value', () => {
expect(
shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />)
).toMatchSnapshot();
expect(shallow(<FacetHeader 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} />)
shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={1} />)
).toMatchSnapshot();
});

it('should render closed facet without value', () => {
expect(
shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />)
).toMatchSnapshot();
expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot();
});

it('should render without link', () => {
expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot();
expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot();
});

it('should call onClick', () => {
const onClick = jest.fn();
const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />);
click(wrapper.find('a'));
expect(onClick).toHaveBeenCalled();
});

it('should clear', () => {
const onClear = jest.fn();
const wrapper = shallow(
<FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} />
<FacetHeader name="foo" onClear={onClear} onClick={jest.fn()} open={false} values={3} />
);
click(wrapper);
expect(onClick).toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
click(wrapper.find('.button-red'));
expect(onClear).toHaveBeenCalled();
});

+ 144
- 105
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap Переглянути файл

@@ -1,132 +1,171 @@
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,
exports[`test should clear 1`] = `
<div>
<button
className="search-navigator-facet-header-button button-small button-red"
onClick={[Function]}>
clear
</button>
<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"
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
<span
className="spacer-left badge is-rounded">
3
</span>
</a>
</div>
`;

exports[`test should render closed facet with value 1`] = `
<div>
<a
className="search-navigator-facet-header"
href="#"
onClick={[Function]}>
<svg
height="10"
style={
Object {
"fill": "currentColor ",
"paddingTop": 3,
}
} />
</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>
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
<span
className="spacer-left badge is-rounded">
1
</span>
</a>
</div>
`;

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"
<div>
<a
className="search-navigator-facet-header"
href="#"
onClick={[Function]}>
<svg
height="10"
style={
Object {
"fill": "currentColor ",
"paddingTop": 3,
}
} />
</svg>
foo
</a>
}
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>
</div>
`;

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"
<div>
<a
className="search-navigator-facet-header"
href="#"
onClick={[Function]}>
<svg
height="10"
style={
Object {
"fill": "currentColor ",
"paddingTop": 3,
}
} />
</svg>
foo
</a>
}
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>
</div>
`;

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"
<div>
<a
className="search-navigator-facet-header"
href="#"
onClick={[Function]}>
<svg
height="10"
style={
Object {
"fill": "currentColor ",
"paddingTop": 3,
}
} />
</svg>
foo
</a>
}
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>
</div>
`;

exports[`test should render without link 1`] = `
<span
className="search-navigator-facet-header">
foo
</span>
<div>
<span
className="search-navigator-facet-header">
foo
</span>
</div>
`;

+ 29
- 2
server/sonar-web/src/main/js/apps/issues/styles.css Переглянути файл

@@ -39,6 +39,12 @@
}
}

.issues-main-header-spinner {
margin-left: 1px;
margin-right: 9px;
margin-top: -1px;
}

.concise-issues-list-header,
.concise-issues-list-header-inner {
}
@@ -88,9 +94,11 @@
transition: background-color 0.3s ease, border-color 0.3s ease;
}

.concise-issue-box:hover,
.concise-issue-box:focus {
.concise-issue-box:hover {
background-color: #ffeaea;
}

.concise-issue-box:focus {
outline: none
}

@@ -102,6 +110,8 @@
}

.concise-issue-box-message {
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
}

@@ -129,4 +139,21 @@
display: flex;
align-items: flex-start;
border: none;
}

.issues-filters-header {
margin-bottom: 12px;
padding-bottom: 11px;
border-bottom: 1px solid #e6e6e6;
}

.issues-my-issues-filter {
margin-bottom: 24px;
text-align: center;
}

.issues-page-actions {
display: inline-block;
min-width: 80px;
text-align: right;
}

+ 9
- 0
server/sonar-web/src/main/js/apps/issues/utils.js Переглянути файл

@@ -164,6 +164,15 @@ type RawFacet = {

export type Facet = { [string]: number };

export const mapFacet = (facet: string): string => {
const propertyMapping = {
files: 'fileUuids',
modules: 'moduleUuids',
projects: 'projectUuids'
};
return propertyMapping[facet] || facet;
};

export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => {
// for readability purpose
const propertyMapping = {

+ 19
- 20
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js Переглянути файл

@@ -24,11 +24,6 @@ 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.PureComponent {
@@ -100,19 +95,23 @@ export default class AllProjects extends React.PureComponent {
const top = this.props.organization ? 95 : 30;

return (
<Page className="projects-page">
<PageSide top={top}>
<PageFilters>
<PageSidebar
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</PageFilters>
</PageSide>
<div className="layout-page projects-page">
<div className="layout-page-side-outer">
<div className="layout-page-side" style={{ top }}>
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<PageSidebar
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>
</div>
</div>
</div>

<PageMain>
<PageMainInner>
<div className="layout-page-main">
<div className="layout-page-main-inner">
<PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
{view === 'list' &&
<ProjectsListContainer
@@ -132,9 +131,9 @@ export default class AllProjects extends React.PureComponent {
sort={query.sort}
visualization={visualization}
/>}
</PageMainInner>
</PageMain>
</Page>
</div>
</div>
</div>
);
}
}

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js Переглянути файл

@@ -180,7 +180,7 @@ export default class SourceViewerBase extends React.PureComponent {
`.source-line-code[data-line-number="${line}"] .source-line-issue-locations`
);
if (lineElement) {
scrollToElement(lineElement, 125, 75);
scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 });
}
}


+ 2
- 2
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js Переглянути файл

@@ -22,7 +22,7 @@ import React from 'react';
import { Link } from 'react-router';
import QualifierIcon from '../shared/QualifierIcon';
import FavoriteContainer from '../controls/FavoriteContainer';
import { getProjectUrl, getIssuesUrl } from '../../helpers/urls';
import { getProjectUrl, getComponentIssuesUrl } from '../../helpers/urls';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
import { translate } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
@@ -171,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent {
<div className="source-viewer-header-measure">
<span className="source-viewer-header-measure-value">
<Link
to={getIssuesUrl({ resolved: 'false', fileUuids: uuid })}
to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid })}
className="source-viewer-header-external-link"
target="_blank">
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}

+ 47
- 50
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js Переглянути файл

@@ -28,7 +28,6 @@ import LineDuplications from './LineDuplications';
import LineDuplicationBlock from './LineDuplicationBlock';
import LineIssuesIndicator from './LineIssuesIndicator';
import LineCode from './LineCode';
import { TooltipsContainer } from '../../mixins/tooltips-mixin';
import type { SourceLine } from '../types';
import type { LinearIssueLocation } from '../helpers/indexing';
import type { Issue } from '../../issue/types';
@@ -99,62 +98,60 @@ export default class Line extends React.PureComponent {
});

return (
<TooltipsContainer>
<tr className={className} data-line-number={line.line}>
<LineNumber line={line} onClick={this.props.onClick} />
<tr className={className} data-line-number={line.line}>
<LineNumber line={line} onClick={this.props.onClick} />

<LineSCM
line={line}
onClick={this.props.onSCMClick}
previousLine={this.props.previousLine}
/>

{this.props.displayCoverage &&
<LineCoverage line={line} onClick={this.props.onCoverageClick} />}

{this.props.displayDuplications &&
<LineDuplications line={line} onClick={this.props.loadDuplications} />}
<LineSCM
line={line}
onClick={this.props.onSCMClick}
previousLine={this.props.previousLine}
/>

{times(duplicationsCount).map(index => (
<LineDuplicationBlock
duplicated={duplications.includes(index)}
index={index}
key={index}
line={this.props.line}
onClick={this.props.onDuplicationClick}
/>
))}
{this.props.displayCoverage &&
<LineCoverage line={line} onClick={this.props.onCoverageClick} />}

{this.props.displayIssues &&
!this.props.displayAllIssues &&
<LineIssuesIndicator
issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>}
{this.props.displayDuplications &&
<LineDuplications line={line} onClick={this.props.loadDuplications} />}

{this.props.displayFiltered &&
<td className="source-meta source-line-filtered-container" data-line-number={line.line}>
<div className="source-line-bar" />
</td>}
{times(duplicationsCount).map(index => (
<LineDuplicationBlock
duplicated={duplications.includes(index)}
index={index}
key={index}
line={this.props.line}
onClick={this.props.onDuplicationClick}
/>
))}

<LineCode
highlightedLocationMessage={this.props.highlightedLocationMessage}
highlightedSymbols={this.props.highlightedSymbols}
{this.props.displayIssues &&
!this.props.displayAllIssues &&
<LineIssuesIndicator
issues={this.props.issues}
issueLocations={this.props.issueLocations}
line={line}
onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
scroll={this.props.scroll}
secondaryIssueLocations={this.props.secondaryIssueLocations}
selectedIssue={this.props.selectedIssue}
showIssues={this.props.openIssues || this.props.displayAllIssues}
/>
</tr>
</TooltipsContainer>
onClick={this.handleIssuesIndicatorClick}
/>}

{this.props.displayFiltered &&
<td className="source-meta source-line-filtered-container" data-line-number={line.line}>
<div className="source-line-bar" />
</td>}

<LineCode
highlightedLocationMessage={this.props.highlightedLocationMessage}
highlightedSymbols={this.props.highlightedSymbols}
issues={this.props.issues}
issueLocations={this.props.issueLocations}
line={line}
onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
scroll={this.props.scroll}
secondaryIssueLocations={this.props.secondaryIssueLocations}
selectedIssue={this.props.selectedIssue}
showIssues={this.props.openIssues || this.props.displayAllIssues}
/>
</tr>
);
}
}

+ 10
- 7
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js Переглянути файл

@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';

@@ -40,21 +41,23 @@ export default class LineCoverage extends React.PureComponent {
const className =
'source-meta source-line-coverage' +
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
const title = line.coverageStatus != null
? translate('source_viewer.tooltip', line.coverageStatus)
: undefined;
return (
const cell = (
<td
className={className}
data-line-number={line.line}
title={title}
data-placement={line.coverageStatus != null ? 'right' : undefined}
data-toggle={line.coverageStatus != null ? 'tooltip' : undefined}
role={line.coverageStatus != null ? 'button' : undefined}
tabIndex={line.coverageStatus != null ? 0 : undefined}
onClick={line.coverageStatus != null ? this.handleClick : undefined}>
<div className="source-line-bar" />
</td>
);

return line.coverageStatus != null
? <Tooltip
overlay={translate('source_viewer.tooltip', line.coverageStatus)}
placement="right">
{cell}
</Tooltip>
: cell;
}
}

+ 8
- 4
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js Переглянути файл

@@ -20,6 +20,7 @@
// @flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';

@@ -44,20 +45,23 @@ export default class LineDuplicationBlock extends React.PureComponent {
'source-line-duplicated': duplicated
});

return (
const cell = (
<td
key={index}
className={className}
data-line-number={line.line}
data-index={index}
title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined}
data-placement={duplicated ? 'right' : undefined}
data-toggle={duplicated ? 'tooltip' : undefined}
role={duplicated ? 'button' : undefined}
tabIndex={duplicated ? '0' : undefined}
onClick={duplicated ? this.handleClick : undefined}>
<div className="source-line-bar" />
</td>
);

return duplicated
? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
{cell}
</Tooltip>
: cell;
}
}

+ 8
- 5
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js Переглянути файл

@@ -20,6 +20,7 @@
// @flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';

@@ -41,19 +42,21 @@ export default class LineDuplications extends React.PureComponent {
const className = classNames('source-meta', 'source-line-duplications', {
'source-line-duplicated': line.duplicated
});
const title = line.duplicated ? translate('source_viewer.tooltip.duplicated_line') : undefined;

return (
const cell = (
<td
className={className}
title={title}
data-placement={line.duplicated ? 'right' : undefined}
data-toggle={line.duplicated ? 'tooltip' : undefined}
role={line.duplicated ? 'button' : undefined}
tabIndex={line.duplicated ? 0 : undefined}
onClick={line.duplicated ? this.handleClick : undefined}>
<div className="source-line-bar" />
</td>
);

return line.duplicated
? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
{cell}
</Tooltip>
: cell;
}
}

+ 2
- 2
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js Переглянути файл

@@ -27,7 +27,7 @@ it('render covered line', () => {
const onClick = jest.fn();
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
click(wrapper);
click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});

@@ -36,7 +36,7 @@ it('render uncovered line', () => {
const onClick = jest.fn();
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
click(wrapper);
click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});


+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js Переглянути файл

@@ -29,7 +29,7 @@ it('render duplicated line', () => {
<LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} />
);
expect(wrapper).toMatchSnapshot();
click(wrapper);
click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});


+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js Переглянути файл

@@ -27,7 +27,7 @@ it('render duplicated line', () => {
const onClick = jest.fn();
const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
click(wrapper);
click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});


+ 26
- 24
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap Переглянути файл

@@ -1,16 +1,17 @@
exports[`test render covered line 1`] = `
<td
className="source-meta source-line-coverage source-line-covered"
data-line-number={3}
data-placement="right"
data-toggle="tooltip"
onClick={[Function]}
role="button"
tabIndex={0}
title="source_viewer.tooltip.covered">
<div
className="source-line-bar" />
</td>
<Tooltip
overlay="source_viewer.tooltip.covered"
placement="right">
<td
className="source-meta source-line-coverage source-line-covered"
data-line-number={3}
onClick={[Function]}
role="button"
tabIndex={0}>
<div
className="source-line-bar" />
</td>
</Tooltip>
`;

exports[`test render line with unknown coverage 1`] = `
@@ -23,16 +24,17 @@ exports[`test render line with unknown coverage 1`] = `
`;

exports[`test render uncovered line 1`] = `
<td
className="source-meta source-line-coverage source-line-uncovered"
data-line-number={3}
data-placement="right"
data-toggle="tooltip"
onClick={[Function]}
role="button"
tabIndex={0}
title="source_viewer.tooltip.uncovered">
<div
className="source-line-bar" />
</td>
<Tooltip
overlay="source_viewer.tooltip.uncovered"
placement="right">
<td
className="source-meta source-line-coverage source-line-uncovered"
data-line-number={3}
onClick={[Function]}
role="button"
tabIndex={0}>
<div
className="source-line-bar" />
</td>
</Tooltip>
`;

+ 14
- 13
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap Переглянути файл

@@ -1,17 +1,18 @@
exports[`test render duplicated line 1`] = `
<td
className="source-meta source-line-duplications-extra source-line-duplicated"
data-index={1}
data-line-number={3}
data-placement="right"
data-toggle="tooltip"
onClick={[Function]}
role="button"
tabIndex="0"
title="source_viewer.tooltip.duplicated_block">
<div
className="source-line-bar" />
</td>
<Tooltip
overlay="source_viewer.tooltip.duplicated_block"
placement="right">
<td
className="source-meta source-line-duplications-extra source-line-duplicated"
data-index={1}
data-line-number={3}
onClick={[Function]}
role="button"
tabIndex="0">
<div
className="source-line-bar" />
</td>
</Tooltip>
`;

exports[`test render not duplicated line 1`] = `

+ 12
- 11
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap Переглянути файл

@@ -1,15 +1,16 @@
exports[`test render duplicated line 1`] = `
<td
className="source-meta source-line-duplications source-line-duplicated"
data-placement="right"
data-toggle="tooltip"
onClick={[Function]}
role="button"
tabIndex={0}
title="source_viewer.tooltip.duplicated_line">
<div
className="source-line-bar" />
</td>
<Tooltip
overlay="source_viewer.tooltip.duplicated_line"
placement="right">
<td
className="source-meta source-line-duplications source-line-duplicated"
onClick={[Function]}
role="button"
tabIndex={0}>
<div
className="source-line-bar" />
</td>
</Tooltip>
`;

exports[`test render not duplicated line 1`] = `

+ 7
- 0
server/sonar-web/src/main/js/components/common/EmptySearch.css Переглянути файл

@@ -0,0 +1,7 @@
.empty-search {
padding: 60px 0;
border: 1px solid #e6e6e6;
border-radius: 2px;
color: #777;
text-align: center;
}

+ 2
- 9
server/sonar-web/src/main/js/components/common/EmptySearch.js Переглянути файл

@@ -19,18 +19,11 @@
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import { translate } from '../../helpers/l10n';
import './EmptySearch.css';

const EmptySearch = () => (
<div
className={css({
padding: '60px 0',
border: '1px solid #e6e6e6',
borderRadius: 2,
textAlign: 'center',
color: '#777'
})}>
<div className="empty-search">
<h3>{translate('no_results_search')}</h3>
<p className="big-spacer-top">{translate('no_results_search.2')}</p>
</div>

+ 12
- 1
server/sonar-web/src/main/js/components/common/SelectList.js Переглянути файл

@@ -36,6 +36,7 @@ type State = {

export default class SelectList extends React.PureComponent {
currentKeyScope: string;
previousFilter: Function;
previousKeyScope: string;
props: Props;
state: State;
@@ -66,9 +67,18 @@ export default class SelectList extends React.PureComponent {

attachShortcuts = () => {
this.previousKeyScope = key.getScope();
this.previousFilter = key.filter;
this.currentKeyScope = uniqueId('key-scope');
key.setScope(this.currentKeyScope);

// sometimes there is a *focused* search field next to the SelectList component
// we need to allow shortcuts in this case, but only for the used keys
key.filter = (event: KeyboardEvent & { target: HTMLElement }) => {
const tagName = (event.target || event.srcElement).tagName;
const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
return [13, 38, 40].includes(event.keyCode) || !isInput;
};

key('down', this.currentKeyScope, () => {
this.setState(this.selectNextElement);
return false;
@@ -80,7 +90,7 @@ export default class SelectList extends React.PureComponent {
});

key('return', this.currentKeyScope, () => {
if (this.state.active) {
if (this.state.active != null) {
this.handleSelect(this.state.active);
}
return false;
@@ -90,6 +100,7 @@ export default class SelectList extends React.PureComponent {
detachShortcuts = () => {
key.setScope(this.previousKeyScope);
key.deleteScope(this.currentKeyScope);
key.filter = this.previousFilter;
};

handleSelect = (item: string) => {

+ 1
- 3
server/sonar-web/src/main/js/components/controls/Checkbox.js Переглянути файл

@@ -44,9 +44,7 @@ export default class Checkbox extends React.PureComponent {
}

render() {
const className = classNames('icon-checkbox', {
// trick to work with glamor
[this.props.className]: true,
const className = classNames('icon-checkbox', this.props.className, {
'icon-checkbox-checked': this.props.checked,
'icon-checkbox-single': this.props.thirdState
});

+ 31
- 26
server/sonar-web/src/main/js/components/issue/Issue.js Переглянути файл

@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
import key from 'keymaster';
import IssueView from './IssueView';
import { updateIssue } from './actions';
import { setIssueAssignee } from '../../api/issues';
@@ -86,11 +87,39 @@ export default class BaseIssue extends React.PureComponent {
}

bindShortcuts() {
document.addEventListener('keypress', this.handleKeyPress);
key('f', 'issues', () => {
this.togglePopup('transition');
return false;
});
key('a', 'issues', () => {
this.togglePopup('assign');
return false;
});
key('m', 'issues', () => {
this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
return false;
});
key('i', 'issues', () => {
this.togglePopup('set-severity');
return false;
});
key('c', 'issues', () => {
this.togglePopup('comment');
return false;
});
key('t', 'issues', () => {
this.togglePopup('edit-tags');
return false;
});
}

unbindShortcuts() {
document.removeEventListener('keypress', this.handleKeyPress);
key.unbind('f', 'issues');
key.unbind('a', 'issues');
key.unbind('m', 'issues');
key.unbind('i', 'issues');
key.unbind('c', 'issues');
key.unbind('t', 'issues');
}

togglePopup = (popupName: string, open?: boolean) => {
@@ -118,30 +147,6 @@ export default class BaseIssue extends React.PureComponent {
onFail(this.context.store.dispatch)(error);
};

handleKeyPress = (e: Object) => {
const tagName = e.target.tagName.toUpperCase();
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';

if (shouldHandle) {
switch (e.key) {
case 'f':
return this.togglePopup('transition');
case 'a':
return this.togglePopup('assign');
case 'm':
return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
case 'p':
return this.togglePopup('plan');
case 'i':
return this.togglePopup('set-severity');
case 'c':
return this.togglePopup('comment');
case 't':
return this.togglePopup('edit-tags');
}
}
};

render() {
return (
<IssueView

+ 5
- 1
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js Переглянути файл

@@ -94,7 +94,10 @@ export default function IssueTitleBar(props: Props) {
<li className="issue-meta">
{onIssuesPage
? locationsBadge
: <Link onClick={stopPropagation} to={getSingleIssueUrl(issue.key)}>
: <Link
onClick={stopPropagation}
target="_blank"
to={getSingleIssueUrl(issue.key)}>
{locationsBadge}
</Link>}
</li>}
@@ -102,6 +105,7 @@ export default function IssueTitleBar(props: Props) {
<Link
className="js-issue-permalink icon-link"
onClick={stopPropagation}
target="_blank"
to={getSingleIssueUrl(issue.key)}
/>
</li>

+ 4
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap Переглянути файл

@@ -49,11 +49,13 @@ exports[`test should render the titlebar correctly 1`] = `
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to={
Object {
"pathname": "/issues",
"query": Object {
"issues": "AVsae-CQS-9G3txfbFN2",
"open": "AVsae-CQS-9G3txfbFN2",
},
}
} />
@@ -116,11 +118,13 @@ exports[`test should render the titlebar with the filter 1`] = `
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to={
Object {
"pathname": "/issues",
"query": Object {
"issues": "AVsae-CQS-9G3txfbFN2",
"open": "AVsae-CQS-9G3txfbFN2",
},
}
} />

+ 2
- 6
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js Переглянути файл

@@ -19,8 +19,6 @@
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import { css } from 'glamor';
import { debounce, map } from 'lodash';
import Avatar from '../../../components/ui/Avatar';
import BubblePopup from '../../../components/common/BubblePopup';
@@ -54,7 +52,6 @@ type State = {
};

const LIST_SIZE = 10;
const USER_MARGIN = css({ marginLeft: '24px' });

export default class SetAssigneePopup extends React.PureComponent {
defaultUsersArray: Array<User>;
@@ -152,9 +149,8 @@ export default class SetAssigneePopup extends React.PureComponent {
size={16}
/>}
<span
className={classNames('vertical-middle', {
[USER_MARGIN]: !(user.avatar || user.email)
})}>
className="vertical-middle"
style={{ marginLeft: !user.avatar && !user.email ? 24 : undefined }}>
{user.name}
</span>
</SelectListItem>

+ 9
- 1
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js Переглянути файл

@@ -37,6 +37,7 @@ type State = {
const LIST_SIZE = 10;

export default class SetIssueTagsPopup extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

@@ -47,15 +48,22 @@ export default class SetIssueTagsPopup extends React.PureComponent {
}

componentDidMount() {
this.mounted = true;
this.onSearch('');
}

componentWillUnmount() {
this.mounted = false;
}

onSearch = (query: string) => {
searchIssueTags({
q: query || '',
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
}).then((tags: Array<string>) => {
this.setState({ searchResult: tags });
if (this.mounted) {
this.setState({ searchResult: tags });
}
}, this.props.onFail);
};


+ 0
- 42
server/sonar-web/src/main/js/components/layout/Page.js Переглянути файл

@@ -1,42 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';

type Props = {
className?: string,
children?: React.Element<*>
};

const styles = css({
display: 'flex',
alignItems: 'stretch',
width: '100%',
flexGrow: 1
});

export default function Page({ className, children, ...other }: Props) {
return (
<div className={styles + (className ? ` ${className}` : '')} {...other}>
{children}
</div>
);
}

+ 0
- 34
server/sonar-web/src/main/js/components/layout/PageFilters.js Переглянути файл

@@ -1,34 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';

type Props = {
children?: React.Element<*>
};

export default function PageSide(props: Props) {
return (
<div className={css({ width: 260, padding: 20 })}>
{props.children}
</div>
);
}

+ 0
- 34
server/sonar-web/src/main/js/components/layout/PageMain.js Переглянути файл

@@ -1,34 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';

type Props = {
children?: React.Element<*>
};

export default function PageMain(props: Props) {
return (
<div className={css({ flexGrow: 1, minWidth: 740, padding: 20 })}>
{props.children}
</div>
);
}

+ 0
- 34
server/sonar-web/src/main/js/components/layout/PageMainInner.js Переглянути файл

@@ -1,34 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';

type Props = {
children?: React.Element<*>
};

export default function PageMainInner(props: Props) {
return (
<div className={css({ minWidth: 740, maxWidth: 980 })}>
{props.children}
</div>
);
}

+ 0
- 73
server/sonar-web/src/main/js/components/layout/PageSide.js Переглянути файл

@@ -1,73 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css, media } from 'glamor';

type Props = {
children?: React.Element<*>,
top?: number
};

const width = css(
{
width: 'calc(50vw - 360px)'
},
media('(max-width: 1320px)', { width: 300 })
);

const sideStyles = css(width, {
flexGrow: 0,
flexShrink: 0,
backgroundColor: '#f3f3f3'
});

const sideStickyStyles = css(width, {
position: 'fixed',
zIndex: 40,
top: 0,
bottom: 0,
left: 0,
borderRight: '1px solid #e6e6e6',
overflowY: 'auto',
overflowX: 'hidden',
backgroundColor: '#f3f3f3'
});

const sideInnerStyles = css(
{
width: 300,
marginLeft: 'calc(50vw - 660px)',
backgroundColor: '#f3f3f3'
},
media('(max-width: 1320px)', { marginLeft: 0 })
);

export default function PageSide(props: Props) {
return (
<div className={sideStyles}>
<div className={`layout-page-side ${sideStickyStyles}`} style={{ top: props.top || 30 }}>
<div className={sideInnerStyles}>
{props.children}
</div>
</div>
</div>
);
}

+ 27
- 9
server/sonar-web/src/main/js/helpers/scrolling.js Переглянути файл

@@ -37,13 +37,12 @@ const scrollElement = (element: HTMLElement, position: number) => {
};

let smoothScrollTop = (y: number, parent) => {
const scrollTop = getScrollPosition(parent);
let scrollTop = getScrollPosition(parent);
const scrollingDown = y > scrollTop;
const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS);
let stepsDone = 0;

const interval = setInterval(() => {
const scrollTop = getScrollPosition(parent);
if (scrollTop === y || SCROLLING_STEPS === stepsDone) {
clearInterval(interval);
} else {
@@ -54,6 +53,7 @@ let smoothScrollTop = (y: number, parent) => {
goal = Math.max(y, scrollTop - step);
}
stepsDone++;
scrollTop = goal;
scrollElement(parent, goal);
}
}, SCROLLING_INTERVAL);
@@ -63,23 +63,41 @@ smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true

export const scrollToElement = (
element: HTMLElement,
topOffset: number = 0,
bottomOffset: number = 0,
parent: HTMLElement = window
options: {
topOffset?: number,
bottomOffset?: number,
parent?: HTMLElement,
smooth?: boolean
}
) => {
const opts = { topOffset: 0, bottomOffset: 0, parent: window, smooth: true, ...options };
const { parent } = opts;

const { top, bottom } = element.getBoundingClientRect();

const scrollTop = getScrollPosition(parent);

const height: number = parent === window
? window.innerHeight
: parent.getBoundingClientRect().height;

const parentTop = parent === window ? 0 : parent.getBoundingClientRect().top;

if (top - parentTop < topOffset) {
smoothScrollTop(scrollTop - topOffset + top - parentTop, parent);
if (top - parentTop < opts.topOffset) {
const goal = scrollTop - opts.topOffset + top - parentTop;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
scrollElement(parent, goal);
}
}

if (bottom - parentTop > height - bottomOffset) {
smoothScrollTop(scrollTop + bottom - parentTop - height + bottomOffset, parent);
if (bottom - parentTop > height - opts.bottomOffset) {
const goal = scrollTop + bottom - parentTop - height + opts.bottomOffset;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
scrollElement(parent, goal);
}
}
};

+ 7
- 1
server/sonar-web/src/main/js/helpers/urls.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 { stringify } from 'querystring';
import { getProfilePath } from '../apps/quality-profiles/utils';

/**
@@ -49,11 +50,16 @@ export function getComponentIssuesUrl(componentKey, query) {
return { pathname: '/project/issues', query: { ...query, id: componentKey } };
}

export function getComponentIssuesUrlAsString(componentKey, query) {
const path = getComponentIssuesUrl(componentKey, query);
return `${window.baseUrl}${path.pathname}?${stringify(path.query)}`;
}

/**
* Generate URL for a single issue
*/
export function getSingleIssueUrl(issues) {
return { pathname: '/issues', query: { issues } };
return { pathname: '/issues', query: { issues, open: issues } };
}

/**

+ 7
- 1
server/sonar-web/src/main/less/components/badges.less Переглянути файл

@@ -26,7 +26,7 @@
min-width: 10px;
padding: 2px 7px;
font-size: 11px;
font-weight: 300;
font-weight: normal;
letter-spacing: 0.03em;
color: @white;
line-height: 12px;
@@ -41,6 +41,12 @@

a& { .link-no-underline; }

&.is-rounded {
padding-left: 5px;
padding-right: 5px;
border-radius: 50px;
}

.list-group-item > &,
.list-group-item-heading > & {
float: right;

+ 2
- 0
server/sonar-web/src/main/less/components/issues.less Переглянути файл

@@ -39,6 +39,8 @@
padding-bottom: @bottomPadding;
border: 1px solid transparent;
background-color: @issueBackgroundColor;
outline: none;
transition: border-color 0.3s ease;
}

.issue-list,

+ 5
- 0
server/sonar-web/src/main/less/components/modals.less Переглянути файл

@@ -174,6 +174,11 @@ ul.modal-head-metadata li {
margin-top: 5px;
margin-bottom: 4px;
}

& > .icon-checkbox {
padding-top: 6px;
padding-right: 8px;
}
}

.modal-field {

+ 63
- 0
server/sonar-web/src/main/less/components/page.less Переглянути файл

@@ -191,3 +191,66 @@
}
}
}

.layout-page {
display: flex;
align-items: stretch;
width: 100%;
flex-grow: 1;
}

.layout-page-filters {
width: 260px;
padding: 20px;
}

.layout-page-main {
flex-grow: 1;
min-width: 740px;
padding: 20px;
}

.layout-page-main-inner {
min-width: 740px;
max-width: 980px;
}

.layout-page-side-outer {
width: ~"calc(50vw - 360px)";
flex-grow: 0;
flex-shrink: 0;
background-color: #f3f3f3;
}

.layout-page-side {
position: fixed;
z-index: 40;
top: 30px;
bottom: 0;
left: 0;
width: ~"calc(50vw - 360px)";
border-right: 1px solid #e6e6e6;
overflow-y: auto;
overflow-x: hidden;
background-color: #f3f3f3;
}

.layout-page-side-inner {
width: 300px;
margin-left: ~"calc(50vw - 660px)";
background-color: #f3f3f3;
}

@media (max-width: 1320px) {
.layout-page-side-outer {
width: 300px;
}

.layout-page-side {
width: 300px;
}

.layout-page-side-inner {
margin-left: 0;
}
}

+ 6
- 0
server/sonar-web/src/main/less/components/search-navigator.less Переглянути файл

@@ -122,6 +122,7 @@
white-space: normal;
overflow: hidden;
font-size: 0;
cursor: not-allowed;
transition: none;

a& {
@@ -309,6 +310,11 @@
font-weight: 600;
}

.search-navigator-facet-header-button {
float: right;
margin-top: 6px;
}

.search-navigator-facet-list {
margin: 0 0 0 0;
padding: 0 10px 10px;

+ 21
- 1
server/sonar-web/src/main/less/pages/issues.less Переглянути файл

@@ -25,7 +25,7 @@


.issues {
&.sticky {

.issues-workspace-list,
@@ -58,6 +58,26 @@
.search-navigator-facet-footer {
padding: 0 0 10px 0;
}

.issue-list {
/* no math, just a good guess */
min-width: 640px;
width: 800px;

@media (max-width: 1320px) {
& {
width: ~"calc(60vw - 40px)";
}
}
}

.issue {
cursor: pointer;

&:hover {
border-color: @issueBorderColor;
}
}
}

.issues-workspace-list-component {

+ 7
- 15
server/sonar-web/yarn.lock Переглянути файл

@@ -814,16 +814,16 @@ babel-register@^6.22.0:
mkdirp "^0.5.1"
source-map-support "^0.4.2"

babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.9.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.23.0, babel-runtime@^6.9.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"

babel-runtime@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
babel-runtime@^6.11.6, babel-runtime@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"
@@ -2381,7 +2381,7 @@ fbjs@0.1.0-alpha.10:
promise "^7.0.3"
whatwg-fetch "^0.9.0"

fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8:
fbjs@^0.8.1, fbjs@^0.8.4:
version "0.8.8"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6"
dependencies:
@@ -2631,14 +2631,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"

glamor@2.20.24:
version "2.20.24"
resolved "https://repox.sonarsource.com/api/npm/npm/glamor/-/glamor-2.20.24.tgz#a299af2eec687322634ba38e4a0854d8743d2041"
dependencies:
babel-runtime "^6.18.0"
fbjs "^0.8.8"
object-assign "^4.1.0"

glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -227,6 +227,7 @@ bulk_change=Bulk Change
bulleted_point=Bulleted point
check_project=Check project
coding_rules=Rules
clear=Clear
clear_all_filters=Clear All Filters
click_to_add_to_favorites=Click to add to favorites
click_to_remove_from_favorites=Click to remove from favorites
@@ -675,7 +676,7 @@ issue.effort=Effort:
issue.x_effort={0} effort
issue.creation_date=Created
issue.filter_similar_issues=Filter Similar Issues
issue.this_issue_involves_x_code_locations=This issue involved {0} code locations
issue.this_issue_involves_x_code_locations=This issue involves {0} code location(s)
issues.return_to_list=Return to List
issues.issues_limit_reached=For usability reasons, only the {0} issues are displayed.
issues.bulk_change=All Issues ({0})
@@ -695,6 +696,7 @@ issues.issues=issues
issues.to_select_issues=to select issues
issues.to_navigate=to navigate
issues.to_navigate_issue_locations=to navigate issue locations
issues.to_switch_flows=to switch flows
issues.leak_period=Leak Period
issues.my_issues=My Issues


Завантаження…
Відмінити
Зберегти