@@ -156,14 +156,14 @@ public class IssueNotificationsTest extends AbstractIssueTest { | |||
assertThat((String) message.getContent()).contains("Severity"); | |||
assertThat((String) message.getContent()).contains("One Issue Per Line (xoo): 17"); | |||
assertThat((String) message.getContent()).contains( | |||
"See it in SonarQube: http://localhost:9000/component_issues?id=sample#createdAt=2015-12-15T00%3A00%3A00%2B"); | |||
"See it in SonarQube: http://localhost:9000/project/issues?id=sample&createdAt=2015-12-15T00%3A00%3A00%2B"); | |||
assertThat(emails.hasNext()).isTrue(); | |||
message = emails.next().getMimeMessage(); | |||
assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>"); | |||
assertThat((String) message.getContent()).contains("sample/Sample.xoo"); | |||
assertThat((String) message.getContent()).contains("Assignee changed to Tester"); | |||
assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key()); | |||
assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key()); | |||
assertThat(emails.hasNext()).isFalse(); | |||
} | |||
@@ -218,7 +218,7 @@ public class IssueNotificationsTest extends AbstractIssueTest { | |||
assertThat((String) message.getContent()).contains("sample/Sample.xoo"); | |||
assertThat((String) message.getContent()).contains("Severity: BLOCKER (was MINOR)"); | |||
assertThat((String) message.getContent()).contains( | |||
"See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key()); | |||
"See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key()); | |||
assertThat(emails.hasNext()).isFalse(); | |||
} |
@@ -28,7 +28,6 @@ import org.apache.commons.lang.time.DateUtils; | |||
import org.assertj.core.api.Fail; | |||
import org.junit.Before; | |||
import org.junit.BeforeClass; | |||
import org.junit.Ignore; | |||
import org.junit.Test; | |||
import org.sonar.wsclient.base.HttpException; | |||
import org.sonar.wsclient.base.Paging; | |||
@@ -50,7 +49,6 @@ import static util.ItUtils.runProjectAnalysis; | |||
import static util.ItUtils.setServerProperty; | |||
import static util.ItUtils.toDate; | |||
import static util.ItUtils.verifyHttpException; | |||
import static util.selenium.Selenese.runSelenese; | |||
public class IssueSearchTest extends AbstractIssueTest { | |||
@@ -273,17 +271,6 @@ public class IssueSearchTest extends AbstractIssueTest { | |||
assertThat(issues.component(issue).projectId()).isEqualTo(project.id()); | |||
} | |||
/** | |||
* SONAR-5659 | |||
*/ | |||
@Test | |||
@Ignore("unstable") | |||
public void redirect_to_search_url_after_wrong_login() { | |||
// Force user authentication to check login on the issues search page | |||
setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "true"); | |||
runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html"); | |||
} | |||
@Test | |||
public void return_issue_type() throws Exception { | |||
List<org.sonarqube.ws.Issues.Issue> issues = searchByRuleKey("xoo:OneBugIssuePerLine"); | |||
@@ -309,11 +296,6 @@ public class IssueSearchTest extends AbstractIssueTest { | |||
assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("VULNERABILITY"))).getPaging().getTotal()).isEqualTo(8); | |||
} | |||
@Test | |||
public void bulk_change() { | |||
runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/bulk_change.html"); | |||
} | |||
private List<org.sonarqube.ws.Issues.Issue> searchByRuleKey(String... ruleKey) throws IOException { | |||
return searchIssues(new SearchWsRequest().setRules(asList(ruleKey))).getIssuesList(); | |||
} |
@@ -87,14 +87,14 @@ public class UiTest { | |||
$(".overview-quality-gate") | |||
.shouldBe(visible) | |||
.shouldHave(text("Passed")); | |||
$("a[href=\"/component_issues?id=sample#resolved=false|types=CODE_SMELL\"]") | |||
$("a[href=\"/project/issues?id=sample&resolved=false&types=CODE_SMELL\"]") | |||
.shouldBe(visible) | |||
.shouldHave(text("0")) | |||
.click(); | |||
// on project issues page | |||
assertThat(url()).contains("/component_issues?id=sample#resolved=false|types=CODE_SMELL"); | |||
$(".facet.active[data-unresolved]").shouldBe(visible); | |||
assertThat(url()).contains("/project/issues?id=sample&resolved=false&types=CODE_SMELL"); | |||
$("[data-property=\"resolutions\"] .facet.active").shouldBe(visible); | |||
$("#global-navigation").find("a[href=\"/profiles\"]").click(); | |||
@@ -19,9 +19,7 @@ | |||
*/ | |||
package pageobjects.issues; | |||
import com.codeborne.selenide.CollectionCondition; | |||
import com.codeborne.selenide.ElementsCollection; | |||
import com.codeborne.selenide.SelenideElement; | |||
import java.util.List; | |||
import java.util.stream.Collectors; | |||
@@ -55,23 +53,14 @@ public class IssuesPage { | |||
public IssuesPage bulkChangeOpen() { | |||
$("#issues-bulk-change").shouldBe(visible).click(); | |||
$("a.js-bulk-change").click(); | |||
$("#bulk-change-form").shouldBe(visible); | |||
return this; | |||
} | |||
public IssuesPage bulkChangeAssigneeSearchCount(String query, Integer count) { | |||
if (!$(".select2-drop-active").isDisplayed()) { | |||
$("#bulk-change-form #s2id_assignee").shouldBe(visible).click(); | |||
} | |||
SelenideElement input = $(".select2-drop-active input").shouldBe(visible); | |||
input.val("").sendKeys(query); | |||
if (count > 0) { | |||
$(".select2-drop-active .select2-results li.select2-result").shouldBe(visible); | |||
} else { | |||
$(".select2-drop-active .select2-results li.select2-no-results").shouldBe(visible); | |||
} | |||
$$(".select2-drop-active .select2-results li.select2-result").shouldHaveSize(count); | |||
$("#issues-bulk-change-assignee .Select-input input").val(query); | |||
$$("#issues-bulk-change-assignee .Select-option").shouldHaveSize(count); | |||
$("#issues-bulk-change-assignee .Select-input input").pressEscape(); | |||
return this; | |||
} | |||
} |
@@ -1,88 +0,0 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |||
<head profile="http://selenium-ide.openqa.org/profiles/test-case"> | |||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> | |||
</head> | |||
<body> | |||
<table cellpadding="1" cellspacing="1" border="1"> | |||
<tbody> | |||
<tr> | |||
<td>open</td> | |||
<td>/sessions/logout</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/sessions/new</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>content</td> | |||
<td>*Log In to SonarQube*</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=login</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=password</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>clickAndWait</td> | |||
<td>commit</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=.js-user-authenticated</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/issues</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=.search-navigator-workspace-list .issue</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>id=issues-bulk-change</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>click</td> | |||
<td>id=issues-bulk-change</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>click</td> | |||
<td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>id=bulk-change-form</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>id=transition-confirm</td> | |||
<td></td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</body> | |||
</html> |
@@ -1,83 +0,0 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |||
<head profile="http://selenium-ide.openqa.org/profiles/test-case"> | |||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> | |||
</head> | |||
<body> | |||
<table cellpadding="1" cellspacing="1" border="1"> | |||
<tbody> | |||
<tr> | |||
<td>open</td> | |||
<td>/sessions/logout</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/issues#resolved=true|statuses=OPEN</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>content</td> | |||
<td>*Log In to SonarQube*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>id=login</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=password</td> | |||
<td>wrongpassword</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=login</td> | |||
<td>wronglogin</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=password</td> | |||
<td>wrongpassword</td> | |||
</tr> | |||
<tr> | |||
<td>clickAndWait</td> | |||
<td>commit</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>css=.alert</td> | |||
<td>*Authentication failed*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>id=login</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=login</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>id=password</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>clickAndWait</td> | |||
<td>commit</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>assertLocation</td> | |||
<td>*#resolved=true|statuses=OPEN*</td> | |||
<td></td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</body> | |||
</html> |
@@ -175,7 +175,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate { | |||
String dateString = notification.getFieldValue(FIELD_PROJECT_DATE); | |||
if (projectKey != null && dateString != null) { | |||
Date date = DateUtils.parseDateTime(dateString); | |||
String url = String.format("%s/component_issues?id=%s#createdAt=%s", | |||
String url = String.format("%s/project/issues?id=%s&createdAt=%s", | |||
settings.getServerBaseURL(), encode(projectKey), encode(DateUtils.formatDateTime(date))); | |||
message | |||
.append("See it in SonarQube: ") |
@@ -102,7 +102,7 @@ public class IssueChangesEmailTemplate extends EmailTemplate { | |||
private void appendFooter(StringBuilder sb, Notification notification) { | |||
String issueKey = notification.getFieldValue("key"); | |||
sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues/search#issues=").append(issueKey).append(NEW_LINE); | |||
sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues?issues=").append(issueKey).append(NEW_LINE); | |||
} | |||
private static void appendLine(StringBuilder sb, @Nullable String line) { |
@@ -60,7 +60,7 @@ public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate { | |||
String assignee = notification.getFieldValue(FIELD_ASSIGNEE); | |||
if (projectUuid != null && dateString != null && assignee != null) { | |||
Date date = DateUtils.parseDateTime(dateString); | |||
String url = String.format("%s/issues/search#projectUuids=%s|assignees=%s|createdAt=%s", | |||
String url = String.format("%s/issues?projectUuids=%s&assignees=%s&createdAt=%s", | |||
settings.getServerBaseURL(), | |||
encode(projectUuid), | |||
encode(assignee), |
@@ -3,4 +3,4 @@ Rule: Avoid Cycles | |||
Message: Has 3 cycles | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE |
@@ -4,4 +4,4 @@ Message: Has 3 cycles | |||
Resolution: FIXED (was FALSE-POSITIVE) | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE |
@@ -4,4 +4,4 @@ Message: Has 3 cycles | |||
Action Plan changed to ABC 1.0 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE |
@@ -4,4 +4,4 @@ Message: Has 3 cycles | |||
Assignee changed to louis | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE |
@@ -9,4 +9,4 @@ Resolution: FALSE-POSITIVE | |||
Status: RESOLVED | |||
Tags: [bug performance] | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE |
@@ -17,4 +17,4 @@ Project: Struts | |||
/path/to/file: 3 | |||
/path/to/directory: 7 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1 |
@@ -5,4 +5,4 @@ Project: Struts | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1 |
@@ -21,4 +21,4 @@ Project: Struts | |||
/path/to/file: 3 | |||
/path/to/directory: 7 | |||
See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1 | |||
See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1 |
@@ -5,4 +5,4 @@ Project: Struts | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1 | |||
See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1 |
@@ -11,7 +11,6 @@ | |||
}, | |||
"globals": { | |||
"key": true, | |||
"baseUrl": true, | |||
"SyntheticInputEvent": true | |||
}, |
@@ -25,7 +25,6 @@ module.exports = { | |||
'handlebars/runtime', | |||
'./src/main/js/libs/third-party/jquery-ui.js', | |||
'./src/main/js/libs/third-party/select2.js', | |||
'./src/main/js/libs/third-party/keymaster.js', | |||
'./src/main/js/libs/third-party/bootstrap/tooltip.js', | |||
'./src/main/js/libs/third-party/bootstrap/dropdown.js' | |||
], | |||
@@ -92,7 +91,7 @@ module.exports = { | |||
{ test: require.resolve('react-dom'), loader: 'expose?ReactDOM' } | |||
] | |||
}, | |||
postcss: function() { | |||
postcss() { | |||
return [autoprefixer(autoprefixerOptions)]; | |||
}, | |||
// Some libraries import Node modules but don't use them in the browser. |
@@ -17,9 +17,11 @@ | |||
"d3-selection": "1.0.5", | |||
"d3-shape": "1.0.6", | |||
"escape-html": "1.0.3", | |||
"glamor": "2.20.24", | |||
"handlebars": "2.0.0", | |||
"history": "2.0.0", | |||
"jquery": "2.2.0", | |||
"keymaster": "1.6.2", | |||
"lodash": "4.6.1", | |||
"moment": "2.10.6", | |||
"numeral": "1.5.3", |
@@ -137,6 +137,9 @@ export function searchProjects(data?: Object) { | |||
return getJSON(url, data); | |||
} | |||
export const searchComponents = (data?: { q?: string, qualifiers?: string, ps?: number }) => | |||
getJSON('/api/components/search', data); | |||
/** | |||
* Change component's key | |||
* @param {string} from |
@@ -41,15 +41,6 @@ type IssuesResponse = { | |||
users?: Array<*> | |||
}; | |||
export type Transition = | |||
| 'confirm' | |||
| 'unconfirm' | |||
| 'reopen' | |||
| 'resolve' | |||
| 'falsepositive' | |||
| 'wontfix' | |||
| 'close'; | |||
export const searchIssues = (query: {}): Promise<IssuesResponse> => | |||
getJSON('/api/issues/search', query); | |||
@@ -97,7 +88,9 @@ export function getIssuesCount(query: {}): Promise<*> { | |||
}); | |||
} | |||
export const searchIssueTags = (ps: number = 500) => getJSON('/api/issues/tags', { ps }); | |||
export const searchIssueTags = ( | |||
data: { ps?: number, q?: string } = { ps: 500 } | |||
): Promise<Array<string>> => getJSON('/api/issues/tags', data).then(r => r.tags); | |||
export function getIssueChangelog(issue: string): Promise<*> { | |||
const url = '/api/issues/changelog'; | |||
@@ -142,7 +135,7 @@ export function setIssueTags(data: { issue: string, tags: string }): Promise<Iss | |||
} | |||
export function setIssueTransition( | |||
data: { issue: string, transition: Transition } | |||
data: { issue: string, transition: string } | |||
): Promise<IssueResponse> { | |||
const url = '/api/issues/do_transition'; | |||
return postJSON(url, data); |
@@ -20,7 +20,7 @@ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import QualifierIcon from '../../../../components/shared/qualifier-icon'; | |||
import QualifierIcon from '../../../../components/shared/QualifierIcon'; | |||
import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer'; | |||
import OrganizationLink from '../../../../components/ui/OrganizationLink'; | |||
@@ -101,11 +101,14 @@ export default class ComponentNavMenu extends React.Component { | |||
); | |||
} | |||
renderComponentIssuesLink() { | |||
renderIssuesLink() { | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/component_issues', query: { id: this.props.component.key } }} | |||
to={{ | |||
pathname: '/project/issues', | |||
query: { id: this.props.component.key, resolved: 'false' } | |||
}} | |||
activeClassName="active"> | |||
{translate('issues.page')} | |||
</Link> | |||
@@ -343,7 +346,7 @@ export default class ComponentNavMenu extends React.Component { | |||
return ( | |||
<ul className="nav navbar-nav nav-tabs"> | |||
{this.renderDashboardLink()} | |||
{this.renderComponentIssuesLink()} | |||
{this.renderIssuesLink()} | |||
{this.renderComponentMeasuresLink()} | |||
{this.renderCodeLink()} | |||
{this.renderActivityLink()} |
@@ -4,7 +4,7 @@ exports[`test should not render breadcrumbs with one element 1`] = ` | |||
<span> | |||
<span | |||
className="navbar-context-title-qualifier little-spacer-right"> | |||
<qualifier-icon | |||
<QualifierIcon | |||
qualifier="TRK" /> | |||
</span> | |||
<Link | |||
@@ -33,7 +33,7 @@ exports[`test should render organization 1`] = ` | |||
<span> | |||
<span | |||
className="navbar-context-title-qualifier little-spacer-right"> | |||
<qualifier-icon | |||
<QualifierIcon | |||
qualifier="TRK" /> | |||
</span> | |||
<OrganizationLink |
@@ -25,9 +25,10 @@ exports[`test should work with extensions 1`] = ` | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/component_issues", | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"id": "foo", | |||
"resolved": "false", | |||
}, | |||
} | |||
}> |
@@ -25,7 +25,10 @@ import { isUserAdmin } from '../../../../helpers/users'; | |||
export default class GlobalNavMenu extends React.Component { | |||
static propTypes = { | |||
appState: React.PropTypes.object.isRequired, | |||
currentUser: React.PropTypes.object.isRequired | |||
currentUser: React.PropTypes.object.isRequired, | |||
location: React.PropTypes.shape({ | |||
pathname: React.PropTypes.string.isRequired | |||
}).isRequired | |||
}; | |||
static defaultProps = { | |||
@@ -59,12 +62,12 @@ export default class GlobalNavMenu extends React.Component { | |||
renderIssuesLink() { | |||
const query = this.props.currentUser.isLoggedIn | |||
? '#resolved=false|assigned_to_me=true' | |||
: '#resolved=false'; | |||
const url = '/issues' + query; | |||
? { myIssues: 'true', resolved: 'false' } | |||
: { resolved: 'false' }; | |||
const active = this.props.location.pathname === 'issues'; | |||
return ( | |||
<li> | |||
<Link to={url} className={this.activeLink('/issues')}> | |||
<Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}> | |||
{translate('issues.page')} | |||
</Link> | |||
</li> |
@@ -20,6 +20,7 @@ | |||
import Backbone from 'backbone'; | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import key from 'keymaster'; | |||
import SearchView from './SearchView'; | |||
import { getCurrentUser } from '../../../../store/rootReducer'; | |||
@@ -253,7 +253,7 @@ export default Marionette.LayoutView.extend({ | |||
getNavigationFindings(q) { | |||
const DEFAULT_ITEMS = [ | |||
{ name: translate('issues.page'), url: window.baseUrl + '/issues/search' }, | |||
{ name: translate('issues.page'), url: window.baseUrl + '/issues' }, | |||
{ | |||
name: translate('layout.measures'), | |||
url: window.baseUrl + '/measures/search?qualifiers[]=TRK' |
@@ -30,6 +30,8 @@ it('should work with extensions', () => { | |||
isLoggedIn: false, | |||
permissions: { global: [] } | |||
}; | |||
const wrapper = shallow(<GlobalNavMenu appState={appState} currentUser={currentUser} />); | |||
const wrapper = shallow( | |||
<GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -12,10 +12,16 @@ exports[`test should work with extensions 1`] = ` | |||
</li> | |||
<li> | |||
<Link | |||
className={null} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/issues#resolved=false"> | |||
to={ | |||
Object { | |||
"pathname": "/issues", | |||
"query": Object { | |||
"resolved": "false", | |||
}, | |||
} | |||
}> | |||
issues.page | |||
</Link> | |||
</li> |
@@ -44,7 +44,6 @@ import backgroundTasksRoutes from '../../apps/background-tasks/routes'; | |||
import codeRoutes from '../../apps/code/routes'; | |||
import codingRulesRoutes from '../../apps/coding-rules/routes'; | |||
import componentRoutes from '../../apps/component/routes'; | |||
import componentIssuesRoutes from '../../apps/component-issues/routes'; | |||
import componentMeasuresRoutes from '../../apps/component-measures/routes'; | |||
import customMeasuresRoutes from '../../apps/custom-measures/routes'; | |||
import groupsRoutes from '../../apps/groups/routes'; | |||
@@ -89,9 +88,8 @@ const startReactApp = () => { | |||
<Router history={history} onUpdate={handleUpdate}> | |||
<Route | |||
path="/account/issues" | |||
onEnter={() => { | |||
const defaultFilter = window.location.hash || '#resolve=false'; | |||
window.location = `${window.baseUrl}/issues${defaultFilter}|assigned_to_me=true`; | |||
onEnter={(_, replace) => { | |||
replace({ pathname: '/issues', query: { myIssues: 'true', resolved: 'false' } }); | |||
}} | |||
/> | |||
@@ -117,6 +115,7 @@ const startReactApp = () => { | |||
/> | |||
<Redirect from="/component/index" to="/component" /> | |||
<Redirect from="/component_issues" to="/project/issues" /> | |||
<Redirect from="/dashboard/index" to="/dashboard" /> | |||
<Redirect from="/governance" to="/view" /> | |||
<Redirect from="/extension/governance/portfolios" to="/portfolios" /> | |||
@@ -158,7 +157,6 @@ const startReactApp = () => { | |||
<Route component={ProjectContainer}> | |||
<Route path="code" childRoutes={codeRoutes} /> | |||
<Route path="component_issues" childRoutes={componentIssuesRoutes} /> | |||
<Route path="component_measures" childRoutes={componentMeasuresRoutes} /> | |||
<Route path="custom_measures" childRoutes={customMeasuresRoutes} /> | |||
<Route path="dashboard" childRoutes={overviewRoutes} /> | |||
@@ -176,6 +174,7 @@ const startReactApp = () => { | |||
component={ProjectPageExtension} | |||
/> | |||
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} /> | |||
<Route path="issues" childRoutes={issuesRoutes} /> | |||
<Route path="settings" childRoutes={settingsRoutes} /> | |||
{projectAdminRoutes} | |||
</Route> |
@@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'BUG' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'BUG' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(bugs, 'SHORT_INT')} | |||
</Link> | |||
@@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(vulnerabilities, 'SHORT_INT')} | |||
</Link> | |||
@@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(codeSmells, 'SHORT_INT')} | |||
</Link> |
@@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'BUG' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'BUG' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(bugs, 'SHORT_INT')} | |||
</Link> | |||
@@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(vulnerabilities, 'SHORT_INT')} | |||
</Link> | |||
@@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component { | |||
<tr> | |||
<td className="about-page-issue-type-number"> | |||
<Link | |||
to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })} | |||
to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })} | |||
className="about-page-issue-type-link"> | |||
{formatMeasure(codeSmells, 'SHORT_INT')} | |||
</Link> |
@@ -21,7 +21,7 @@ | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import TaskType from './TaskType'; | |||
import QualifierIcon from '../../../components/shared/qualifier-icon'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import Organization from '../../../components/shared/Organization'; | |||
import { Task } from '../types'; | |||
@@ -20,7 +20,7 @@ | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import Truncated from './Truncated'; | |||
import QualifierIcon from '../../../components/shared/qualifier-icon'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
function getTooltip(component) { | |||
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import key from 'keymaster'; | |||
import Controller from '../../components/navigator/controller'; | |||
import Rule from './models/rule'; | |||
import RuleDetailsView from './rule-details-view'; |
@@ -22,6 +22,7 @@ import $ from 'jquery'; | |||
import { sortBy } from 'lodash'; | |||
import Backbone from 'backbone'; | |||
import Marionette from 'backbone.marionette'; | |||
import key from 'keymaster'; | |||
import State from './models/state'; | |||
import Layout from './layout'; | |||
import Rules from './models/rules'; | |||
@@ -105,7 +106,7 @@ App.on('start', function(options: { | |||
}); | |||
this.layout.filtersRegion.show(this.filtersView); | |||
window.key.setScope('list'); | |||
key.setScope('list'); | |||
this.router = new Router({ | |||
app: this | |||
}); |
@@ -21,6 +21,7 @@ import $ from 'jquery'; | |||
import { union } from 'lodash'; | |||
import Backbone from 'backbone'; | |||
import Marionette from 'backbone.marionette'; | |||
import key from 'keymaster'; | |||
import Rules from './models/rules'; | |||
import MetaView from './rule/rule-meta-view'; | |||
import DescView from './rule/rule-description-view'; |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import key from 'keymaster'; | |||
import WorkspaceListView from '../../components/navigator/workspace-list-view'; | |||
import WorkspaceListItemView from './workspace-list-item-view'; | |||
import WorkspaceListEmptyView from './workspace-list-empty-view'; |
@@ -1,130 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import { difference } from 'lodash'; | |||
import Backbone from 'backbone'; | |||
import Marionette from 'backbone.marionette'; | |||
import State from '../issues/models/state'; | |||
import Layout from '../issues/layout'; | |||
import Issues from '../issues/models/issues'; | |||
import Facets from '../../components/navigator/models/facets'; | |||
import Controller from '../issues/controller'; | |||
import Router from '../issues/router'; | |||
import WorkspaceListView from '../issues/workspace-list-view'; | |||
import WorkspaceHeaderView from '../issues/workspace-header-view'; | |||
import FacetsView from './../issues/facets-view'; | |||
import HeaderView from './../issues/HeaderView'; | |||
const App = new Marionette.Application(); | |||
const init = function({ el, component, currentUser }) { | |||
this.config = { | |||
resource: component.id, | |||
resourceName: component.name, | |||
resourceQualifier: component.qualifier | |||
}; | |||
this.state = new State({ | |||
canBulkChange: currentUser.isLoggedIn, | |||
isContext: true, | |||
contextQuery: { componentUuids: this.config.resource }, | |||
contextComponentUuid: this.config.resource, | |||
contextComponentName: this.config.resourceName, | |||
contextComponentQualifier: this.config.resourceQualifier, | |||
contextOrganization: component.organization | |||
}); | |||
this.updateContextFacets(); | |||
this.list = new Issues(); | |||
this.facets = new Facets(); | |||
this.layout = new Layout({ app: this, el }); | |||
this.layout.render(); | |||
$('#footer').addClass('search-navigator-footer'); | |||
this.controller = new Controller({ app: this }); | |||
this.issuesView = new WorkspaceListView({ | |||
app: this, | |||
collection: this.list | |||
}); | |||
this.layout.workspaceListRegion.show(this.issuesView); | |||
this.issuesView.bindScrollEvents(); | |||
this.workspaceHeaderView = new WorkspaceHeaderView({ | |||
app: this, | |||
collection: this.list | |||
}); | |||
this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView); | |||
this.facetsView = new FacetsView({ | |||
app: this, | |||
collection: this.facets | |||
}); | |||
this.layout.facetsRegion.show(this.facetsView); | |||
this.headerView = new HeaderView({ | |||
app: this | |||
}); | |||
this.layout.filtersRegion.show(this.headerView); | |||
key.setScope('list'); | |||
App.router = new Router({ app: App }); | |||
Backbone.history.start(); | |||
}; | |||
App.getContextQuery = function() { | |||
return { componentUuids: this.config.resource }; | |||
}; | |||
App.getRestrictedFacets = function() { | |||
return { | |||
TRK: ['projectUuids'], | |||
BRC: ['projectUuids'], | |||
DIR: ['projectUuids', 'moduleUuids', 'directories'], | |||
DEV: ['authors'], | |||
DEV_PRJ: ['projectUuids', 'authors'] | |||
}; | |||
}; | |||
App.updateContextFacets = function() { | |||
const facets = this.state.get('facets'); | |||
const allFacets = this.state.get('allFacets'); | |||
const facetsFromServer = this.state.get('facetsFromServer'); | |||
return this.state.set({ | |||
facets, | |||
allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]), | |||
facetsFromServer: difference( | |||
facetsFromServer, | |||
this.getRestrictedFacets()[this.config.resourceQualifier] | |||
) | |||
}); | |||
}; | |||
App.on('start', options => { | |||
init.call(App, options); | |||
}); | |||
export default function(el, component, currentUser) { | |||
App.start({ el, component, currentUser }); | |||
return () => { | |||
Backbone.history.stop(); | |||
App.layout.destroy(); | |||
$('#footer').removeClass('search-navigator-footer'); | |||
}; | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import QualifierIcon from '../../../../components/shared/qualifier-icon'; | |||
import QualifierIcon from '../../../../components/shared/QualifierIcon'; | |||
import { isDiffMetric, formatLeak } from '../../utils'; | |||
import { formatMeasure } from '../../../../helpers/measures'; | |||
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import QualifierIcon from '../../../../components/shared/qualifier-icon'; | |||
import QualifierIcon from '../../../../components/shared/QualifierIcon'; | |||
import { splitPath } from '../../../../helpers/path'; | |||
import { getComponentUrl } from '../../../../helpers/urls'; | |||
@@ -1,303 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { debounce, sortBy } from 'lodash'; | |||
import ModalForm from '../../components/common/modal-form'; | |||
import Template from './templates/BulkChangeForm.hbs'; | |||
import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; | |||
import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues'; | |||
import { searchUsers } from '../../api/users'; | |||
import { searchMembers } from '../../api/organizations'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
const LIMIT = 500; | |||
const INPUT_WIDTH = '250px'; | |||
const MINIMUM_QUERY_LENGTH = 2; | |||
const UNASSIGNED = '<UNASSIGNED>'; | |||
type Issue = { | |||
actions?: Array<string>, | |||
assignee: string | null, | |||
transitions?: Array<string> | |||
}; | |||
const hasAction = (action: string) => | |||
(issue: Issue) => issue.actions && issue.actions.includes(action); | |||
export default ModalForm.extend({ | |||
template: Template, | |||
initialize() { | |||
this.issues = null; | |||
this.paging = null; | |||
this.tags = null; | |||
this.loadIssues(); | |||
this.loadTags(); | |||
}, | |||
loadIssues() { | |||
const { query } = this.options; | |||
searchIssues({ | |||
...query, | |||
additionalFields: 'actions,transitions', | |||
ps: LIMIT | |||
}).then(r => { | |||
this.issues = r.issues; | |||
this.paging = r.paging; | |||
this.render(); | |||
}); | |||
}, | |||
loadTags() { | |||
searchIssueTags().then(r => { | |||
this.tags = r.tags; | |||
this.render(); | |||
}); | |||
}, | |||
assigneeSearch(defaultOptions) { | |||
const { context } = this.options; | |||
return debounce( | |||
query => { | |||
if (query.term.length === 0) { | |||
query.callback({ results: defaultOptions }); | |||
} else if (query.term.length >= MINIMUM_QUERY_LENGTH) { | |||
const onSuccess = r => { | |||
query.callback({ | |||
results: r.users.map(user => ({ | |||
id: user.login, | |||
text: `${user.name} (${user.login})` | |||
})) | |||
}); | |||
}; | |||
if (context.isContext) { | |||
searchMembers({ organization: context.organization, q: query.term }).then(onSuccess); | |||
} else { | |||
searchUsers(query.term).then(onSuccess); | |||
} | |||
} | |||
}, | |||
250 | |||
); | |||
}, | |||
prepareAssigneeSelect() { | |||
const input = this.$('#assignee'); | |||
if (input.length) { | |||
const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues); | |||
const currentUser = getCurrentUserFromStore(); | |||
const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues); | |||
const defaultOptions = []; | |||
if (canBeAssignedToMe && currentUser.isLoggedIn) { | |||
defaultOptions.push({ | |||
id: currentUser.login, | |||
text: `${currentUser.name} (${currentUser.login})` | |||
}); | |||
} | |||
if (canBeUnassigned) { | |||
defaultOptions.push({ id: UNASSIGNED, text: translate('unassigned') }); | |||
} | |||
input.select2({ | |||
allowClear: false, | |||
placeholder: translate('search_verb'), | |||
width: INPUT_WIDTH, | |||
formatNoMatches: () => translate('select2.noMatches'), | |||
formatSearching: () => translate('select2.searching'), | |||
formatInputTooShort: () => | |||
translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH), | |||
query: this.assigneeSearch(defaultOptions) | |||
}); | |||
input.on('change', () => this.$('#assign-action').prop('checked', true)); | |||
} | |||
}, | |||
prepareTypeSelect() { | |||
this.$('#type') | |||
.select2({ | |||
minimumResultsForSearch: 999, | |||
width: INPUT_WIDTH | |||
}) | |||
.on('change', () => this.$('#set-type-action').prop('checked', true)); | |||
}, | |||
prepareSeveritySelect() { | |||
const format = state => | |||
state.id | |||
? `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}` | |||
: state.text; | |||
this.$('#severity') | |||
.select2({ | |||
minimumResultsForSearch: 999, | |||
width: INPUT_WIDTH, | |||
formatResult: format, | |||
formatSelection: format | |||
}) | |||
.on('change', () => this.$('#set-severity-action').prop('checked', true)); | |||
}, | |||
prepareTagsInput() { | |||
this.$('#add_tags') | |||
.select2({ | |||
width: INPUT_WIDTH, | |||
tags: this.tags | |||
}) | |||
.on('change', () => this.$('#add-tags-action').prop('checked', true)); | |||
this.$('#remove_tags') | |||
.select2({ | |||
width: INPUT_WIDTH, | |||
tags: this.tags | |||
}) | |||
.on('change', () => this.$('#remove-tags-action').prop('checked', true)); | |||
}, | |||
onRender() { | |||
ModalForm.prototype.onRender.apply(this, arguments); | |||
this.prepareAssigneeSelect(); | |||
this.prepareTypeSelect(); | |||
this.prepareSeveritySelect(); | |||
this.prepareTagsInput(); | |||
}, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
const query = {}; | |||
const assignee = this.$('#assignee').val(); | |||
if (this.$('#assign-action').is(':checked') && assignee != null) { | |||
query['assign'] = assignee === UNASSIGNED ? '' : assignee; | |||
} | |||
const type = this.$('#type').val(); | |||
if (this.$('#set-type-action').is(':checked') && type) { | |||
query['set_type'] = type; | |||
} | |||
const severity = this.$('#severity').val(); | |||
if (this.$('#set-severity-action').is(':checked') && severity) { | |||
query['set_severity'] = severity; | |||
} | |||
const addedTags = this.$('#add_tags').val(); | |||
if (this.$('#add-tags-action').is(':checked') && addedTags) { | |||
query['add_tags'] = addedTags; | |||
} | |||
const removedTags = this.$('#remove_tags').val(); | |||
if (this.$('#remove-tags-action').is(':checked') && removedTags) { | |||
query['remove_tags'] = removedTags; | |||
} | |||
const transition = this.$('[name="do_transition.transition"]:checked').val(); | |||
if (transition) { | |||
query['do_transition'] = transition; | |||
} | |||
const comment = this.$('#comment').val(); | |||
if (comment) { | |||
query['comment'] = comment; | |||
} | |||
const sendNotifications = this.$('#send-notifications').is(':checked'); | |||
if (sendNotifications) { | |||
query['sendNotifications'] = sendNotifications; | |||
} | |||
this.disableForm(); | |||
this.showSpinner(); | |||
const issueKeys = this.issues.map(issue => issue.key); | |||
bulkChangeIssues(issueKeys, query).then( | |||
() => { | |||
this.destroy(); | |||
this.options.onChange(); | |||
}, | |||
(e: Object) => { | |||
this.enableForm(); | |||
this.hideSpinner(); | |||
e.response.json().then(r => this.showErrors(r.errors, r.warnings)); | |||
} | |||
); | |||
}, | |||
canBeAssigned(issues: Array<Issue>) { | |||
return issues.filter(hasAction('assign')).length; | |||
}, | |||
canBeAssignedToMe(issues: Array<Issue>) { | |||
return issues.filter(hasAction('assign_to_me')).length; | |||
}, | |||
canBeUnassigned(issues: Array<Issue>) { | |||
return issues.filter(issue => issue.assignee).length; | |||
}, | |||
canChangeType(issues: Array<Issue>) { | |||
return issues.filter(hasAction('set_type')).length; | |||
}, | |||
canChangeSeverity(issues: Array<Issue>) { | |||
return issues.filter(hasAction('set_severity')).length; | |||
}, | |||
canChangeTags(issues: Array<Issue>) { | |||
return issues.filter(hasAction('set_tags')).length; | |||
}, | |||
canBeCommented(issues: Array<Issue>) { | |||
return issues.filter(hasAction('comment')).length; | |||
}, | |||
availableTransitions(issues: Array<Issue>) { | |||
const transitions = {}; | |||
issues.forEach(issue => { | |||
if (issue.transitions) { | |||
issue.transitions.forEach(t => { | |||
if (transitions[t] != null) { | |||
transitions[t]++; | |||
} else { | |||
transitions[t] = 1; | |||
} | |||
}); | |||
} | |||
}); | |||
return sortBy(Object.keys(transitions)).map(transition => ({ | |||
transition, | |||
count: transitions[transition] | |||
})); | |||
}, | |||
serializeData() { | |||
return { | |||
...ModalForm.prototype.serializeData.apply(this, arguments), | |||
isLoaded: this.issues != null && this.tags != null, | |||
issues: this.issues, | |||
limitReached: this.paging && this.paging.total > LIMIT, | |||
canBeAssigned: this.issues && this.canBeAssigned(this.issues), | |||
canChangeType: this.issues && this.canChangeType(this.issues), | |||
canChangeSeverity: this.issues && this.canChangeSeverity(this.issues), | |||
canChangeTags: this.issues && this.canChangeTags(this.issues), | |||
canBeCommented: this.issues && this.canBeCommented(this.issues), | |||
availableTransitions: this.issues && this.availableTransitions(this.issues) | |||
}; | |||
} | |||
}); |
@@ -1,60 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import Marionette from 'backbone.marionette'; | |||
import Template from './templates/facets/issues-my-issues-facet.hbs'; | |||
export default Marionette.ItemView.extend({ | |||
template: Template, | |||
className: 'issues-header-inner', | |||
events: { | |||
'change [name="issues-page-my"]': 'onMyIssuesChange' | |||
}, | |||
initialize() { | |||
this.listenTo(this.options.app.state, 'change:query', this.render); | |||
}, | |||
onMyIssuesChange() { | |||
const mode = this.$('[name="issues-page-my"]:checked').val(); | |||
if (mode === 'my') { | |||
this.options.app.state.updateFilter({ | |||
assigned_to_me: 'true', | |||
assignees: null, | |||
assigned: null | |||
}); | |||
} else { | |||
this.options.app.state.updateFilter({ | |||
assigned_to_me: null, | |||
assignees: null, | |||
assigned: null | |||
}); | |||
} | |||
}, | |||
serializeData() { | |||
const me = !!this.options.app.state.get('query').assigned_to_me; | |||
return { | |||
...Marionette.ItemView.prototype.serializeData.apply(this, arguments), | |||
me, | |||
isContext: this.options.app.state.get('isContext'), | |||
user: this.options.app.state.get('user') | |||
}; | |||
} | |||
}); |
@@ -1,132 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import React from 'react'; | |||
import { render, unmountComponentAtNode } from 'react-dom'; | |||
import Marionette from 'backbone.marionette'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import WithStore from '../../../components/shared/WithStore'; | |||
export default Marionette.ItemView.extend({ | |||
template() { | |||
return '<div></div>'; | |||
}, | |||
initialize(options) { | |||
this.handleLoadIssues = this.handleLoadIssues.bind(this); | |||
this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this); | |||
this.selectIssue = this.selectIssue.bind(this); | |||
this.listenTo(options.app.state, 'change:selectedIndex', this.select); | |||
}, | |||
onRender() { | |||
this.showViewer(); | |||
}, | |||
onDestroy() { | |||
this.unbindShortcuts(); | |||
unmountComponentAtNode(this.el); | |||
}, | |||
handleLoadIssues(component: string) { | |||
// TODO fromLine: number, toLine: number | |||
const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component); | |||
return Promise.resolve(issues); | |||
}, | |||
showViewer(onLoaded) { | |||
if (!this.baseIssue) { | |||
return; | |||
} | |||
const componentKey = this.baseIssue.get('component'); | |||
render( | |||
<WithStore> | |||
<SourceViewer | |||
aroundLine={this.baseIssue.get('line')} | |||
component={componentKey} | |||
displayAllIssues={true} | |||
loadIssues={this.handleLoadIssues} | |||
onLoaded={onLoaded} | |||
onIssueSelect={this.selectIssue} | |||
selectedIssue={this.baseIssue.get('key')} | |||
/> | |||
</WithStore>, | |||
this.el | |||
); | |||
}, | |||
openFileByIssue(issue) { | |||
this.baseIssue = issue; | |||
this.selectedIssue = issue.get('key'); | |||
this.showViewer(this.scrollToBaseIssue); | |||
this.bindShortcuts(); | |||
}, | |||
bindShortcuts() { | |||
key('up', 'componentViewer', () => { | |||
this.options.app.controller.selectPrev(); | |||
return false; | |||
}); | |||
key('down', 'componentViewer', () => { | |||
this.options.app.controller.selectNext(); | |||
return false; | |||
}); | |||
key('left,backspace', 'componentViewer', () => { | |||
this.options.app.controller.closeComponentViewer(); | |||
return false; | |||
}); | |||
}, | |||
unbindShortcuts() { | |||
key.deleteScope('componentViewer'); | |||
}, | |||
select() { | |||
const selected = this.options.app.state.get('selectedIndex'); | |||
const selectedIssue = this.options.app.list.at(selected); | |||
if (selectedIssue.get('component') === this.baseIssue.get('component')) { | |||
this.baseIssue = selectedIssue; | |||
this.showViewer(this.scrollToBaseIssue); | |||
this.scrollToBaseIssue(); | |||
} else { | |||
this.options.app.controller.showComponentViewer(selectedIssue); | |||
} | |||
}, | |||
scrollToLine(line) { | |||
const row = this.$(`[data-line-number=${line}]`); | |||
const topOffset = $(window).height() / 2 - 60; | |||
const goal = row.length > 0 ? row.offset().top - topOffset : 0; | |||
$(window).scrollTop(goal); | |||
}, | |||
selectIssue(issueKey) { | |||
const issue = this.options.app.list.find(model => model.get('key') === issueKey); | |||
const index = this.options.app.list.indexOf(issue); | |||
this.options.app.state.set({ selectedIndex: index }); | |||
}, | |||
scrollToBaseIssue() { | |||
this.scrollToLine(this.baseIssue.get('line')); | |||
} | |||
}); |
@@ -0,0 +1,649 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import key from 'keymaster'; | |||
import { keyBy, without } from 'lodash'; | |||
import HeaderPanel from './HeaderPanel'; | |||
import PageActions from './PageActions'; | |||
import FiltersHeader from './FiltersHeader'; | |||
import MyIssuesFilter from './MyIssuesFilter'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import IssuesList from './IssuesList'; | |||
import ComponentBreadcrumbs from './ComponentBreadcrumbs'; | |||
import IssuesSourceViewer from './IssuesSourceViewer'; | |||
import BulkChangeModal from './BulkChangeModal'; | |||
import { | |||
parseQuery, | |||
areMyIssuesSelected, | |||
areQueriesEqual, | |||
getOpen, | |||
serializeQuery, | |||
parseFacets | |||
} from '../utils'; | |||
import type { | |||
Query, | |||
Paging, | |||
Facet, | |||
ReferencedComponent, | |||
ReferencedUser, | |||
ReferencedLanguage, | |||
Component, | |||
CurrentUser | |||
} from '../utils'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import EmptySearch from '../../../components/common/EmptySearch'; | |||
import Page from '../../../components/layout/Page'; | |||
import PageMain from '../../../components/layout/PageMain'; | |||
import PageMainInner from '../../../components/layout/PageMainInner'; | |||
import PageSide from '../../../components/layout/PageSide'; | |||
import PageFilters from '../../../components/layout/PageFilters'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
type Props = { | |||
component?: Component, | |||
currentUser: CurrentUser, | |||
fetchIssues: () => Promise<*>, | |||
location: { pathname: string, query: { [string]: string } }, | |||
onRequestFail: (Error) => void, | |||
router: { push: () => void, replace: () => void } | |||
}; | |||
type State = { | |||
bulkChange: 'all' | 'selected' | null, | |||
checked: Array<string>, | |||
facets: { [string]: Facet }, | |||
issues: Array<Issue>, | |||
loading: boolean, | |||
myIssues: boolean, | |||
openFacets: { [string]: boolean }, | |||
paging?: Paging, | |||
query: Query, | |||
referencedComponents: { [string]: ReferencedComponent }, | |||
referencedLanguages: { [string]: ReferencedLanguage }, | |||
referencedRules: { [string]: { name: string } }, | |||
referencedUsers: { [string]: ReferencedUser }, | |||
selected?: string | |||
}; | |||
const DEFAULT_QUERY = { resolved: 'false' }; | |||
export default class App extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
bulkChange: null, | |||
checked: [], | |||
facets: {}, | |||
issues: [], | |||
loading: true, | |||
myIssues: areMyIssuesSelected(props.location.query), | |||
openFacets: { resolutions: true, types: true }, | |||
query: parseQuery(props.location.query), | |||
referencedComponents: {}, | |||
referencedLanguages: {}, | |||
referencedRules: {}, | |||
referencedUsers: {}, | |||
selected: getOpen(props.location.query) | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
const footer = document.getElementById('footer'); | |||
if (footer) { | |||
footer.classList.add('search-navigator-footer'); | |||
} | |||
this.attachShortcuts(); | |||
this.fetchFirstIssues(); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
const open = getOpen(nextProps.location.query); | |||
if (open != null && open !== this.state.selected) { | |||
this.setState({ selected: open }); | |||
} | |||
this.setState({ | |||
myIssues: areMyIssuesSelected(nextProps.location.query), | |||
query: parseQuery(nextProps.location.query) | |||
}); | |||
} | |||
componentDidUpdate(prevProps: Props, prevState: State) { | |||
const { query } = this.props.location; | |||
const { query: prevQuery } = prevProps.location; | |||
if ( | |||
!areQueriesEqual(prevQuery, query) || | |||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) | |||
) { | |||
this.fetchFirstIssues(); | |||
} else if (prevState.selected !== this.state.selected) { | |||
const open = getOpen(query); | |||
if (!open) { | |||
this.scrollToSelectedIssue(); | |||
} | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.detachShortcuts(); | |||
const footer = document.getElementById('footer'); | |||
if (footer) { | |||
footer.classList.remove('search-navigator-footer'); | |||
} | |||
this.mounted = false; | |||
} | |||
attachShortcuts() { | |||
key.setScope('issues'); | |||
key('up', 'issues', () => { | |||
this.selectPreviousIssue(); | |||
return false; | |||
}); | |||
key('down', 'issues', () => { | |||
this.selectNextIssue(); | |||
return false; | |||
}); | |||
key('right', 'issues', () => { | |||
this.openSelectedIssue(); | |||
return false; | |||
}); | |||
key('left', 'issues', () => { | |||
this.closeIssue(); | |||
return false; | |||
}); | |||
} | |||
detachShortcuts() { | |||
key.deleteScope('issues'); | |||
} | |||
getSelectedIndex(): ?number { | |||
const { issues, selected } = this.state; | |||
const index = issues.findIndex(issue => issue.key === selected); | |||
return index !== -1 ? index : null; | |||
} | |||
selectNextIssue = () => { | |||
const { issues } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
if (issues != null && selectedIndex != null && selectedIndex < issues.length - 1) { | |||
if (getOpen(this.props.location.query)) { | |||
this.openIssue(issues[selectedIndex + 1].key); | |||
} else { | |||
this.setState({ selected: issues[selectedIndex + 1].key }); | |||
} | |||
} | |||
}; | |||
selectPreviousIssue = () => { | |||
const { issues } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
if (issues != null && selectedIndex != null && selectedIndex > 0) { | |||
if (getOpen(this.props.location.query)) { | |||
this.openIssue(issues[selectedIndex - 1].key); | |||
} else { | |||
this.setState({ selected: issues[selectedIndex - 1].key }); | |||
} | |||
} | |||
}; | |||
openIssue = (issue: string) => { | |||
const path = { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: issue | |||
} | |||
}; | |||
const open = getOpen(this.props.location.query); | |||
if (open) { | |||
this.props.router.replace(path); | |||
} else { | |||
this.props.router.push(path); | |||
} | |||
}; | |||
closeIssue = () => { | |||
if (this.state.query) { | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: undefined | |||
} | |||
}); | |||
} | |||
}; | |||
openSelectedIssue = () => { | |||
const { selected } = this.state; | |||
if (selected) { | |||
this.openIssue(selected); | |||
} | |||
}; | |||
scrollToSelectedIssue = () => { | |||
const { selected } = this.state; | |||
if (selected) { | |||
const element = document.querySelector(`[data-issue="${selected}"]`); | |||
if (element) { | |||
scrollToElement(element, 150, 100); | |||
} | |||
} | |||
}; | |||
fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => { | |||
const { component } = this.props; | |||
const { myIssues, query } = this.state; | |||
const parameters = { | |||
componentKeys: component && component.key, | |||
...serializeQuery(query), | |||
s: 'FILE_LINE', | |||
ps: 25, | |||
facets: requestFacets | |||
? [ | |||
'assignees', | |||
'authors', | |||
'createdAt', | |||
'directories', | |||
'fileUuids', | |||
'languages', | |||
'moduleUuids', | |||
'projectUuids', | |||
'resolutions', | |||
'rules', | |||
'severities', | |||
'statuses', | |||
'tags', | |||
'types' | |||
].join() | |||
: undefined, | |||
...additional | |||
}; | |||
if (myIssues) { | |||
Object.assign(parameters, { assignees: '__me__' }); | |||
} | |||
return this.props.fetchIssues(parameters); | |||
}; | |||
fetchFirstIssues() { | |||
this.setState({ loading: true }); | |||
this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => { | |||
if (this.mounted) { | |||
const open = getOpen(this.props.location.query); | |||
this.setState({ | |||
facets: parseFacets(facets), | |||
loading: false, | |||
issues, | |||
paging, | |||
referencedComponents: keyBy(other.components, 'uuid'), | |||
referencedLanguages: keyBy(other.languages, 'key'), | |||
referencedRules: keyBy(other.rules, 'key'), | |||
referencedUsers: keyBy(other.users, 'login'), | |||
selected: issues.length > 0 | |||
? issues.find(issue => issue.key === open) != null ? open : issues[0].key | |||
: undefined | |||
}); | |||
} | |||
}); | |||
} | |||
fetchIssuesPage = (p: number): Promise<*> => { | |||
return this.fetchIssues({ p }); | |||
}; | |||
fetchIssuesUntil = (p: number, done: (Array<Issue>, Paging) => boolean) => { | |||
return this.fetchIssuesPage(p).then(response => { | |||
const { issues, paging } = response; | |||
return done(issues, paging) | |||
? { issues, paging } | |||
: this.fetchIssuesUntil(p + 1, done).then(nextResponse => { | |||
return { | |||
issues: [...issues, ...nextResponse.issues], | |||
paging: nextResponse.paging | |||
}; | |||
}); | |||
}); | |||
}; | |||
fetchMoreIssues = () => { | |||
const { paging } = this.state; | |||
if (!paging) { | |||
return; | |||
} | |||
const p = paging.pageIndex + 1; | |||
this.setState({ loading: true }); | |||
this.fetchIssuesPage(p).then(response => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
loading: false, | |||
issues: [...state.issues, ...response.issues], | |||
paging: response.paging | |||
})); | |||
} | |||
}); | |||
}; | |||
fetchIssuesForComponent = (): Promise<Array<Issue>> => { | |||
const { issues, paging } = this.state; | |||
const open = getOpen(this.props.location.query); | |||
const openIssue = issues.find(issue => issue.key === open); | |||
if (!openIssue || !paging) { | |||
return Promise.reject(); | |||
} | |||
const isSameComponent = (issue: Issue): boolean => issue.component === openIssue.component; | |||
const done = (issues: Array<Issue>, paging: Paging): boolean => | |||
paging.total <= paging.pageIndex * paging.pageSize || | |||
issues[issues.length - 1].component !== openIssue.component; | |||
if (done(issues, paging)) { | |||
return Promise.resolve(issues.filter(isSameComponent)); | |||
} | |||
this.setState({ loading: true }); | |||
return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(response => { | |||
const nextIssues = [...issues, ...response.issues]; | |||
this.setState({ | |||
issues: nextIssues, | |||
loading: false, | |||
paging: response.paging | |||
}); | |||
return nextIssues.filter(isSameComponent); | |||
}); | |||
}; | |||
isFiltered = () => { | |||
const serialized = serializeQuery(this.state.query); | |||
return !areQueriesEqual(serialized, DEFAULT_QUERY); | |||
}; | |||
getCheckedIssues = () => { | |||
const issues = this.state.checked.map(checked => | |||
this.state.issues.find(issue => issue.key === checked)); | |||
const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length }; | |||
return Promise.resolve({ issues, paging }); | |||
}; | |||
handleFilterChange = (changes: {}) => { | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, ...changes }), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
}); | |||
}; | |||
handleMyIssuesChange = (myIssues: boolean) => { | |||
this.closeFacet('assignees'); | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: myIssues ? 'true' : undefined | |||
} | |||
}); | |||
}; | |||
closeFacet = (property: string) => { | |||
this.setState(state => ({ | |||
openFacets: { ...state.openFacets, [property]: false } | |||
})); | |||
}; | |||
handleFacetToggle = (property: string) => { | |||
this.setState(state => ({ | |||
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] } | |||
})); | |||
}; | |||
handleReset = () => { | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...DEFAULT_QUERY, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
}); | |||
}; | |||
handleIssueCheck = (issue: string) => { | |||
this.setState(state => ({ | |||
checked: state.checked.includes(issue) | |||
? without(state.checked, issue) | |||
: [...state.checked, issue] | |||
})); | |||
}; | |||
handleIssueChange = (issue: Issue) => { | |||
this.setState(state => ({ | |||
issues: state.issues.map(candidate => candidate.key === issue.key ? issue : candidate) | |||
})); | |||
}; | |||
openBulkChange = (mode: 'all' | 'selected') => { | |||
this.setState({ bulkChange: mode }); | |||
key.setScope('issues-bulk-change'); | |||
}; | |||
closeBulkChange = () => { | |||
key.setScope('issues'); | |||
this.setState({ bulkChange: null }); | |||
}; | |||
handleBulkChangeClick = (e: Event & { target: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
this.openBulkChange('all'); | |||
}; | |||
handleBulkChangeSelectedClick = (e: Event & { target: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
this.openBulkChange('selected'); | |||
}; | |||
handleBulkChangeDone = () => { | |||
this.fetchFirstIssues(); | |||
this.closeBulkChange(); | |||
}; | |||
renderBulkChange(openIssue?: Issue) { | |||
const { component, currentUser } = this.props; | |||
const { bulkChange, checked, paging } = this.state; | |||
if (!currentUser.isLoggedIn || openIssue != null) { | |||
return null; | |||
} | |||
return ( | |||
<div className="pull-left"> | |||
{checked.length > 0 | |||
? <div className="dropdown"> | |||
<button id="issues-bulk-change" data-toggle="dropdown"> | |||
{translate('bulk_change')} | |||
<i className="icon-dropdown little-spacer-left" /> | |||
</button> | |||
<ul className="dropdown-menu"> | |||
<li> | |||
<a href="#" onClick={this.handleBulkChangeClick}> | |||
{translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} | |||
</a> | |||
</li> | |||
<li> | |||
<a href="#" onClick={this.handleBulkChangeSelectedClick}> | |||
{translateWithParameters('issues.bulk_change_selected', checked.length)} | |||
</a> | |||
</li> | |||
</ul> | |||
</div> | |||
: <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}> | |||
{translate('bulk_change')} | |||
</button>} | |||
{bulkChange != null && | |||
<BulkChangeModal | |||
component={component} | |||
currentUser={currentUser} | |||
fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues} | |||
onClose={this.closeBulkChange} | |||
onDone={this.handleBulkChangeDone} | |||
onRequestFail={this.props.onRequestFail} | |||
/>} | |||
</div> | |||
); | |||
} | |||
renderList(openIssue?: Issue) { | |||
const { component, currentUser } = this.props; | |||
const { issues, paging } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null; | |||
if (paging == null) { | |||
return null; | |||
} | |||
return ( | |||
<div className={openIssue != null ? 'hidden' : undefined}> | |||
{paging.total > 0 && | |||
<IssuesList | |||
checked={this.state.checked} | |||
component={component} | |||
issues={issues} | |||
onFilterChange={this.handleFilterChange} | |||
onIssueChange={this.handleIssueChange} | |||
onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined} | |||
onIssueClick={this.openIssue} | |||
selectedIssue={selectedIssue} | |||
/>} | |||
{paging.total > 0 && | |||
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />} | |||
{paging.total === 0 && <EmptySearch />} | |||
</div> | |||
); | |||
} | |||
render() { | |||
const { component, currentUser } = this.props; | |||
const { issues, paging, query } = this.state; | |||
const open = getOpen(this.props.location.query); | |||
const openIssue = issues.find(issue => issue.key === open); | |||
const selectedIndex = this.getSelectedIndex(); | |||
const top = component ? 95 : 30; | |||
return ( | |||
<Page className="issues" id="issues-page"> | |||
<Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" /> | |||
<PageSide top={top}> | |||
<PageFilters> | |||
{currentUser.isLoggedIn && | |||
<MyIssuesFilter | |||
myIssues={this.state.myIssues} | |||
onMyIssuesChange={this.handleMyIssuesChange} | |||
/>} | |||
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} /> | |||
<Sidebar | |||
component={component} | |||
facets={this.state.facets} | |||
myIssues={this.state.myIssues} | |||
onFacetToggle={this.handleFacetToggle} | |||
onFilterChange={this.handleFilterChange} | |||
openFacets={this.state.openFacets} | |||
query={query} | |||
referencedComponents={this.state.referencedComponents} | |||
referencedLanguages={this.state.referencedLanguages} | |||
referencedRules={this.state.referencedRules} | |||
referencedUsers={this.state.referencedUsers} | |||
/> | |||
</PageFilters> | |||
</PageSide> | |||
<PageMain> | |||
<HeaderPanel border={true} top={top}> | |||
<PageMainInner> | |||
{this.renderBulkChange(openIssue)} | |||
{openIssue != null && | |||
<div className="pull-left"> | |||
<ComponentBreadcrumbs component={component} issue={openIssue} /> | |||
</div>} | |||
<PageActions | |||
loading={this.state.loading} | |||
openIssue={openIssue} | |||
paging={paging} | |||
selectedIndex={selectedIndex} | |||
/> | |||
</PageMainInner> | |||
</HeaderPanel> | |||
<PageMainInner> | |||
<div> | |||
{openIssue != null && | |||
<IssuesSourceViewer | |||
openIssue={openIssue} | |||
loadIssues={this.fetchIssuesForComponent} | |||
onIssueChange={this.handleIssueChange} | |||
onIssueSelect={this.openIssue} | |||
/>} | |||
{this.renderList(openIssue)} | |||
</div> | |||
</PageMainInner> | |||
</PageMain> | |||
</Page> | |||
); | |||
} | |||
} |
@@ -17,36 +17,38 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
// @flow | |||
import { connect } from 'react-redux'; | |||
import init from '../init'; | |||
import { withRouter } from 'react-router'; | |||
import type { Dispatch } from 'redux'; | |||
import App from './App'; | |||
import { onFail } from '../../../store/rootActions'; | |||
import { getComponent, getCurrentUser } from '../../../store/rootReducer'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
class ComponentIssuesAppContainer extends React.Component { | |||
componentDidMount() { | |||
this.stop = init(this.refs.container, this.props.component, this.props.currentUser); | |||
} | |||
componentWillUnmount() { | |||
this.stop(); | |||
} | |||
render() { | |||
// placing container inside div is required, | |||
// because when backbone.marionette's layout is destroyed, | |||
// it also destroys the root element, | |||
// but react wants it to be there to unmount it | |||
return ( | |||
<div> | |||
<div ref="container" /> | |||
</div> | |||
); | |||
} | |||
} | |||
type Query = { [string]: string }; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
component: ownProps.location.query.id | |||
? getComponent(state, ownProps.location.query.id) | |||
: undefined, | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps)(ComponentIssuesAppContainer); | |||
const fetchIssues = (query: Query) => | |||
(dispatch: Dispatch<*>) => | |||
searchIssues({ ...query, additionalFields: '_all' }).then( | |||
response => { | |||
const parsedIssues = response.issues.map(issue => | |||
parseIssueFromResponse(issue, response.components, response.users, response.rules)); | |||
return { ...response, issues: parsedIssues }; | |||
}, | |||
onFail(dispatch) | |||
); | |||
const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error); | |||
const mapDispatchToProps = { fetchIssues, onRequestFail }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App)); |
@@ -0,0 +1,522 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import Select from 'react-select'; | |||
import { css } from 'glamor'; | |||
import { pickBy, sortBy } from 'lodash'; | |||
import SearchSelect from './SearchSelect'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import MarkdownTips from '../../../components/common/MarkdownTips'; | |||
import SeverityHelper from '../../../components/shared/SeverityHelper'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; | |||
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { searchAssignees } from '../utils'; | |||
import type { Paging, Component, CurrentUser } from '../utils'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
type Props = {| | |||
component?: Component, | |||
currentUser: CurrentUser, | |||
fetchIssues: ({}) => Promise<*>, | |||
onClose: () => void, | |||
onDone: () => void, | |||
onRequestFail: (Error) => void | |||
|}; | |||
type State = {| | |||
issues: Array<Issue>, | |||
// used for initial loading of issues | |||
loading: boolean, | |||
paging?: Paging, | |||
// used when submitting a form | |||
submitting: boolean, | |||
tags?: Array<string>, | |||
// form fields | |||
addTags?: Array<string>, | |||
assignee?: string, | |||
comment?: string, | |||
notifications?: boolean, | |||
removeTags?: Array<string>, | |||
severity?: string, | |||
transition?: string, | |||
type?: string | |||
|}; | |||
const hasAction = (action: string) => | |||
(issue: Issue): boolean => issue.actions && issue.actions.includes(action); | |||
export default class BulkChangeModal extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { issues: [], loading: true, submitting: false }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
Promise.all([this.loadIssues(), searchIssueTags()]).then(([issues, tags]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
issues: issues.issues, | |||
loading: false, | |||
paging: issues.paging, | |||
tags | |||
}); | |||
} | |||
}); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleCloseClick = (e: Event & { target: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
this.props.onClose(); | |||
}; | |||
loadIssues = () => { | |||
return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 }); | |||
}; | |||
handleAssigneeSearch = (query: string) => { | |||
if (query.length > 1) { | |||
return searchAssignees(query, this.props.component); | |||
} else { | |||
const { currentUser } = this.props; | |||
const { issues } = this.state; | |||
const options = []; | |||
if (currentUser.isLoggedIn) { | |||
const canBeAssignedToMe = issues.filter( | |||
issue => issue.assignee !== currentUser.login | |||
).length > 0; | |||
if (canBeAssignedToMe) { | |||
options.push({ | |||
email: currentUser.email, | |||
label: currentUser.name, | |||
value: currentUser.login | |||
}); | |||
} | |||
} | |||
const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0; | |||
if (canBeUnassigned) { | |||
options.push({ label: translate('unassigned'), value: '' }); | |||
} | |||
return Promise.resolve(options); | |||
} | |||
}; | |||
handleAssigneeSelect = (assignee: string) => { | |||
this.setState({ assignee }); | |||
}; | |||
handleFieldCheck = (field: string) => | |||
(checked: boolean) => { | |||
if (!checked) { | |||
this.setState({ [field]: undefined }); | |||
} else if (field === 'notifications') { | |||
this.setState({ [field]: true }); | |||
} | |||
}; | |||
handleFieldChange = (field: string) => | |||
(event: { target: HTMLInputElement }) => { | |||
this.setState({ [field]: event.target.value }); | |||
}; | |||
handleSelectFieldChange = (field: string) => | |||
({ value }: { value: string }) => { | |||
this.setState({ [field]: value }); | |||
}; | |||
handleMultiSelectFieldChange = (field: string) => | |||
(options: Array<{ value: string }>) => { | |||
this.setState({ [field]: options.map(option => option.value) }); | |||
}; | |||
handleSubmit = (e: Event) => { | |||
e.preventDefault(); | |||
const query = pickBy({ | |||
assign: this.state.assignee, | |||
set_type: this.state.type, | |||
set_severity: this.state.severity, | |||
add_tags: this.state.addTags && this.state.addTags.join(), | |||
remove_tags: this.state.removeTags && this.state.removeTags.join(), | |||
do_transition: this.state.transition, | |||
comment: this.state.comment, | |||
sendNotifications: this.state.notifications | |||
}); | |||
const issueKeys = this.state.issues.map(issue => issue.key); | |||
this.setState({ submitting: true }); | |||
bulkChangeIssues(issueKeys, query).then( | |||
() => { | |||
this.setState({ submitting: false }); | |||
this.props.onDone(); | |||
}, | |||
(error: Error) => { | |||
this.setState({ submitting: false }); | |||
this.props.onRequestFail(error); | |||
} | |||
); | |||
}; | |||
getAvailableTransitions(issues: Array<Issue>): Array<{ transition: string, count: number }> { | |||
const transitions = {}; | |||
issues.forEach(issue => { | |||
if (issue.transitions) { | |||
issue.transitions.forEach(t => { | |||
if (transitions[t] != null) { | |||
transitions[t]++; | |||
} else { | |||
transitions[t] = 1; | |||
} | |||
}); | |||
} | |||
}); | |||
return sortBy(Object.keys(transitions)).map(transition => ({ | |||
transition, | |||
count: transitions[transition] | |||
})); | |||
} | |||
renderCancelButton = () => ( | |||
<a id="bulk-change-cancel" href="#" onClick={this.handleCloseClick}> | |||
{translate('cancel')} | |||
</a> | |||
); | |||
renderLoading = () => ( | |||
<div> | |||
<div className="modal-head"> | |||
<h2>{translate('bulk_change')}</h2> | |||
</div> | |||
<div className="modal-body"> | |||
<div className="text-center"> | |||
<i className="spinner spinner-margin" /> | |||
</div> | |||
</div> | |||
<div className="modal-foot"> | |||
{this.renderCancelButton()} | |||
</div> | |||
</div> | |||
); | |||
renderCheckbox = (field: string) => ( | |||
<Checkbox | |||
className={css({ paddingTop: 6, paddingRight: 8 })} | |||
checked={this.state[field] != null} | |||
onCheck={this.handleFieldCheck(field)} | |||
/> | |||
); | |||
renderAffected = (affected: number) => ( | |||
<div className="pull-right note"> | |||
({translateWithParameters('issue_bulk_change.x_issues', affected)}) | |||
</div> | |||
); | |||
renderField = (field: string, label: string, affected: ?number, input: Object) => ( | |||
<div className="modal-field" id={`issues-bulk-change-${field}`}> | |||
<label htmlFor={field}>{translate(label)}</label> | |||
{this.renderCheckbox(field)} | |||
{input} | |||
{affected != null && this.renderAffected(affected)} | |||
</div> | |||
); | |||
renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => ( | |||
<span> | |||
{(option.avatar != null || option.email != null) && | |||
<Avatar | |||
className="little-spacer-right" | |||
email={option.email} | |||
hash={option.avatar} | |||
size={16} | |||
/>} | |||
{option.label} | |||
</span> | |||
); | |||
renderAssigneeField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('assign')).length; | |||
if (affected === 0) { | |||
return null; | |||
} | |||
const input = ( | |||
<SearchSelect | |||
onSearch={this.handleAssigneeSearch} | |||
onSelect={this.handleAssigneeSelect} | |||
minimumQueryLength={0} | |||
renderOption={this.renderAssigneeOption} | |||
resetOnBlur={false} | |||
value={this.state.assignee} | |||
/> | |||
); | |||
return this.renderField('assignee', 'issue.assign.formlink', affected, input); | |||
}; | |||
renderTypeField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('set_type')).length; | |||
if (affected === 0) { | |||
return null; | |||
} | |||
const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; | |||
const options = types.map(type => ({ label: translate('issue.type', type), value: type })); | |||
const optionRenderer = (option: { label: string, value: string }) => ( | |||
<span> | |||
<IssueTypeIcon className="little-spacer-right" query={option.value} /> | |||
{option.label} | |||
</span> | |||
); | |||
const input = ( | |||
<Select | |||
clearable={false} | |||
id="type" | |||
onChange={this.handleSelectFieldChange('type')} | |||
options={options} | |||
optionRenderer={optionRenderer} | |||
searchable={false} | |||
value={this.state.type} | |||
valueRenderer={optionRenderer} | |||
/> | |||
); | |||
return this.renderField('type', 'issue.set_type', affected, input); | |||
}; | |||
renderSeverityField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('set_severity')).length; | |||
if (affected === 0) { | |||
return null; | |||
} | |||
const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; | |||
const options = severities.map(severity => ({ | |||
label: translate('severity', severity), | |||
value: severity | |||
})); | |||
const input = ( | |||
<Select | |||
clearable={false} | |||
id="severity" | |||
onChange={this.handleSelectFieldChange('severity')} | |||
options={options} | |||
optionRenderer={option => <SeverityHelper severity={option.value} />} | |||
searchable={false} | |||
value={this.state.severity} | |||
valueRenderer={option => <SeverityHelper severity={option.value} />} | |||
/> | |||
); | |||
return this.renderField('severity', 'issue.set_severity', affected, input); | |||
}; | |||
renderAddTagsField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('set_tags')).length; | |||
if (this.state.tags == null || affected === 0) { | |||
return null; | |||
} | |||
const options = this.state.tags.map(tag => ({ label: tag, value: tag })); | |||
const input = ( | |||
<Select | |||
clearable={false} | |||
id="add_tags" | |||
multi={true} | |||
onChange={this.handleMultiSelectFieldChange('addTags')} | |||
options={options} | |||
searchable={true} | |||
value={this.state.addTags} | |||
/> | |||
); | |||
return this.renderField('addTags', 'issue.add_tags', affected, input); | |||
}; | |||
renderRemoveTagsField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('set_tags')).length; | |||
if (this.state.tags == null || affected === 0) { | |||
return null; | |||
} | |||
const options = this.state.tags.map(tag => ({ label: tag, value: tag })); | |||
const input = ( | |||
<Select | |||
clearable={false} | |||
id="remove_tags" | |||
multi={true} | |||
onChange={this.handleMultiSelectFieldChange('removeTags')} | |||
options={options} | |||
searchable={true} | |||
value={this.state.removeTags} | |||
/> | |||
); | |||
return this.renderField('removeTags', 'issue.remove_tags', affected, input); | |||
}; | |||
renderTransitionsField = () => { | |||
const transitions = this.getAvailableTransitions(this.state.issues); | |||
if (transitions.length === 0) { | |||
return null; | |||
} | |||
return ( | |||
<div className="modal-field"> | |||
<label>{translate('issue.transition')}</label> | |||
{transitions.map(transition => ( | |||
<span key={transition.transition}> | |||
<input | |||
checked={this.state.transition === transition.transition} | |||
id={`transition-${transition.transition}`} | |||
name="do_transition.transition" | |||
onChange={this.handleFieldChange('transition')} | |||
type="radio" | |||
value={transition.transition} | |||
/> | |||
<label | |||
htmlFor={`transition-${transition.transition}`} | |||
style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}> | |||
{translate('issue.transition', transition.transition)} | |||
</label> | |||
{this.renderAffected(transition.count)} | |||
<br /> | |||
</span> | |||
))} | |||
</div> | |||
); | |||
}; | |||
renderCommentField = () => { | |||
const affected: number = this.state.issues.filter(hasAction('comment')).length; | |||
if (affected === 0) { | |||
return null; | |||
} | |||
return ( | |||
<div className="modal-field"> | |||
<label htmlFor="comment"> | |||
{translate('issue.comment.formlink')} | |||
<Tooltip overlay={translate('issue_bulk_change.comment.help')}> | |||
<i className="icon-help little-spacer-left" /> | |||
</Tooltip> | |||
</label> | |||
<div> | |||
<textarea | |||
id="comment" | |||
onChange={this.handleFieldChange('comment')} | |||
rows="4" | |||
style={{ width: '100%' }} | |||
value={this.state.comment || ''} | |||
/> | |||
</div> | |||
<div className="pull-right"> | |||
<MarkdownTips /> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderNotificationsField = () => ( | |||
<div className="modal-field"> | |||
<label htmlFor="send-notifications">{translate('issue.send_notifications')}</label> | |||
{this.renderCheckbox('notifications')} | |||
</div> | |||
); | |||
renderForm = () => { | |||
const { issues, paging, submitting } = this.state; | |||
const limitReached: boolean = paging != null && | |||
paging.total > paging.pageIndex * paging.pageSize; | |||
return ( | |||
<form id="bulk-change-form" onSubmit={this.handleSubmit}> | |||
<div className="modal-head"> | |||
<h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2> | |||
</div> | |||
<div className="modal-body"> | |||
{limitReached && | |||
<div className="alert alert-warning"> | |||
{translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)} | |||
</div>} | |||
{this.renderAssigneeField()} | |||
{this.renderTypeField()} | |||
{this.renderSeverityField()} | |||
{this.renderAddTagsField()} | |||
{this.renderRemoveTagsField()} | |||
{this.renderTransitionsField()} | |||
{this.renderCommentField()} | |||
{this.renderNotificationsField()} | |||
</div> | |||
<div className="modal-foot"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<button disabled={submitting} id="bulk-change-submit">{translate('apply')}</button> | |||
{this.renderCancelButton()} | |||
</div> | |||
</form> | |||
); | |||
}; | |||
render() { | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel="modal" | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.props.onClose}> | |||
{this.state.loading ? this.renderLoading() : this.renderForm()} | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import Organization from '../../../components/shared/Organization'; | |||
import { collapsePath, limitComponentName } from '../../../helpers/path'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import type { Component } from '../utils'; | |||
type Props = { | |||
component?: Component, | |||
issue: Object | |||
}; | |||
export default class ComponentBreadcrumbs extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
const { component, issue } = this.props; | |||
const displayOrganization = component == null || ['VW', 'SVW'].includes(component.qualifier); | |||
const displayProject = component == null || | |||
!['TRK', 'BRC', 'DIR'].includes(component.qualifier); | |||
const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier); | |||
return ( | |||
<div className="component-name"> | |||
{displayOrganization && | |||
<Organization linkClassName="link-no-underline" organizationKey={issue.organization} />} | |||
{displayProject && | |||
<span> | |||
<Link to={getProjectUrl(issue.project)} className="link-no-underline"> | |||
{limitComponentName(issue.projectName)} | |||
</Link> | |||
<span className="slash-separator" /> | |||
</span>} | |||
{displaySubProject && | |||
issue.subProject != null && | |||
<span> | |||
<Link to={getProjectUrl(issue.subProject)} className="link-no-underline"> | |||
{limitComponentName(issue.subProjectName)} | |||
</Link> | |||
<span className="slash-separator" /> | |||
</span>} | |||
<Link to={getProjectUrl(issue.component)} className="link-no-underline"> | |||
{collapsePath(issue.componentLongName)} | |||
</Link> | |||
</div> | |||
); | |||
} | |||
} |
@@ -17,39 +17,39 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import init from '../init'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { css } from 'glamor'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class IssuesAppContainer extends React.Component { | |||
static propTypes = { | |||
currentUser: React.PropTypes.any.isRequired | |||
}; | |||
type Props = { | |||
displayReset: boolean, | |||
onReset: () => void | |||
}; | |||
componentDidMount() { | |||
this.stop = init(this.refs.container, this.props.currentUser); | |||
} | |||
const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' }); | |||
componentWillUnmount() { | |||
this.stop(); | |||
} | |||
export default class FiltersHeader extends React.PureComponent { | |||
props: Props; | |||
handleResetClick = (e: Event & { currentTarget: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
this.props.onReset(); | |||
}; | |||
render() { | |||
// placing container inside div is required, | |||
// because when backbone.marionette's layout is destroyed, | |||
// it also destroys the root element, | |||
// but react wants it to be there to unmount it | |||
return ( | |||
<div> | |||
<div ref="container" /> | |||
<div className={styles}> | |||
{this.props.displayReset && | |||
<div className={css({ float: 'right' })}> | |||
<button className="button-red" onClick={this.handleResetClick}> | |||
{translate('clear_all_filters')} | |||
</button> | |||
</div>} | |||
<h3>{translate('filters')}</h3> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps)(IssuesAppContainer); |
@@ -0,0 +1,106 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css, media } from 'glamor'; | |||
import { clearfix } from 'glamor/utils'; | |||
import { throttle } from 'lodash'; | |||
type Props = {| | |||
border: boolean, | |||
children?: React.Element<*>, | |||
top?: number | |||
|}; | |||
type State = { | |||
scrolled: boolean | |||
}; | |||
export default class HeaderPanel extends React.PureComponent { | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { scrolled: this.isScrolled() }; | |||
this.handleScroll = throttle(this.handleScroll, 50); | |||
} | |||
componentDidMount() { | |||
if (this.props.top != null) { | |||
window.addEventListener('scroll', this.handleScroll); | |||
} | |||
} | |||
componentWillUnmount() { | |||
if (this.props.top != null) { | |||
window.removeEventListener('scroll', this.handleScroll); | |||
} | |||
} | |||
isScrolled = () => window.scrollY > 10; | |||
handleScroll = () => { | |||
this.setState({ scrolled: this.isScrolled() }); | |||
}; | |||
render() { | |||
const commonStyles = { | |||
height: 56, | |||
lineHeight: '24px', | |||
padding: '16px 20px', | |||
boxSizing: 'border-box', | |||
borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined, | |||
backgroundColor: '#f3f3f3' | |||
}; | |||
const inner = this.props.top | |||
? <div | |||
className={css( | |||
commonStyles, | |||
{ | |||
position: 'fixed', | |||
zIndex: 30, | |||
top: this.props.top, | |||
left: 'calc(50vw - 360px + 1px)', | |||
right: 0, | |||
boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none', | |||
transition: 'box-shadow 0.3s ease' | |||
}, | |||
media('(max-width: 1320px)', { left: 301 }) | |||
)}> | |||
{this.props.children} | |||
</div> | |||
: this.props.children; | |||
return ( | |||
<div | |||
className={css(clearfix(), commonStyles, { | |||
marginTop: -20, | |||
marginBottom: 20, | |||
marginLeft: -20, | |||
marginRight: -20, | |||
'& .component-name': { lineHeight: '24px' } | |||
})}> | |||
{inner} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import ListItem from './ListItem'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
import type { Component } from '../utils'; | |||
type Props = {| | |||
checked: Array<string>, | |||
component?: Component, | |||
issues: Array<Issue>, | |||
onFilterChange: (changes: {}) => void, | |||
onIssueChange: (Issue) => void, | |||
onIssueCheck?: (string) => void, | |||
onIssueClick: (string) => void, | |||
selectedIssue: ?Issue | |||
|}; | |||
export default class IssuesList extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
const { checked, component, issues, selectedIssue } = this.props; | |||
return ( | |||
<div> | |||
{issues.map((issue, index) => ( | |||
<ListItem | |||
checked={checked.includes(issue.key)} | |||
component={component} | |||
key={issue.key} | |||
issue={issue} | |||
onChange={this.props.onIssueChange} | |||
onCheck={this.props.onIssueCheck} | |||
onClick={this.props.onIssueClick} | |||
onFilterChange={this.props.onFilterChange} | |||
previousIssue={index > 0 ? issues[index - 1] : null} | |||
selected={selectedIssue != null && selectedIssue.key === issue.key} | |||
/> | |||
))} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
type Props = {| | |||
loadIssues: () => Promise<*>, | |||
onIssueChange: (Issue) => void, | |||
onIssueSelect: (string) => void, | |||
openIssue: Issue | |||
|}; | |||
export default class IssuesSourceViewer extends React.PureComponent { | |||
node: HTMLElement; | |||
props: Props; | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.openIssue.component === this.props.openIssue.component) { | |||
this.scrollToIssue(); | |||
} | |||
} | |||
scrollToIssue = () => { | |||
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`); | |||
if (element) { | |||
scrollToElement(element, 100, 100); | |||
} | |||
}; | |||
render() { | |||
const { openIssue } = this.props; | |||
return ( | |||
<div ref={node => this.node = node}> | |||
<SourceViewer | |||
aroundLine={openIssue.line} | |||
component={openIssue.component} | |||
displayAllIssues={true} | |||
loadIssues={this.props.loadIssues} | |||
onLoaded={this.scrollToIssue} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssueSelect={this.props.onIssueSelect} | |||
selectedIssue={openIssue.key} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,106 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import ComponentBreadcrumbs from './ComponentBreadcrumbs'; | |||
import Issue from '../../../components/issue/Issue'; | |||
import type { Issue as IssueType } from '../../../components/issue/types'; | |||
import type { Component } from '../utils'; | |||
type Props = {| | |||
checked: boolean, | |||
component?: Component, | |||
issue: IssueType, | |||
onChange: (IssueType) => void, | |||
onCheck?: (string) => void, | |||
onClick: (string) => void, | |||
onFilterChange: (changes: {}) => void, | |||
previousIssue: ?Object, | |||
selected: boolean | |||
|}; | |||
type State = { | |||
similarIssues: boolean | |||
}; | |||
export default class ListItem extends React.PureComponent { | |||
props: Props; | |||
state: State = { similarIssues: false }; | |||
handleFilter = (property: string, issue: IssueType) => { | |||
const { onFilterChange } = this.props; | |||
const issuesReset = { issues: [] }; | |||
if (property.startsWith('tag###')) { | |||
const tag = property.substr(6); | |||
return onFilterChange({ ...issuesReset, tags: [tag] }); | |||
} | |||
switch (property) { | |||
case 'type': | |||
return onFilterChange({ ...issuesReset, types: [issue.type] }); | |||
case 'severity': | |||
return onFilterChange({ ...issuesReset, severities: [issue.severity] }); | |||
case 'status': | |||
return onFilterChange({ ...issuesReset, statuses: [issue.status] }); | |||
case 'resolution': | |||
return issue.resolution != null | |||
? onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] }) | |||
: onFilterChange({ ...issuesReset, resolved: false, resolutions: [] }); | |||
case 'assignee': | |||
return issue.assignee != null | |||
? onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] }) | |||
: onFilterChange({ ...issuesReset, assigned: false, assignees: [] }); | |||
case 'rule': | |||
return onFilterChange({ ...issuesReset, rules: [issue.rule] }); | |||
case 'project': | |||
return onFilterChange({ ...issuesReset, projects: [issue.projectUuid] }); | |||
case 'module': | |||
return onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] }); | |||
case 'file': | |||
return onFilterChange({ ...issuesReset, files: [issue.componentUuid] }); | |||
} | |||
}; | |||
render() { | |||
const { component, issue, previousIssue } = this.props; | |||
const displayComponent = previousIssue == null || previousIssue.component !== issue.component; | |||
return ( | |||
<div className="issues-workspace-list-item"> | |||
{displayComponent && | |||
<div className="issues-workspace-list-component"> | |||
<ComponentBreadcrumbs component={component} issue={this.props.issue} /> | |||
</div>} | |||
<Issue | |||
checked={this.props.checked} | |||
issue={issue} | |||
onChange={this.props.onChange} | |||
onCheck={this.props.onCheck} | |||
onClick={this.props.onClick} | |||
onFilter={this.handleFilter} | |||
selected={this.props.selected} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,60 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
myIssues: boolean, | |||
onMyIssuesChange: (boolean) => void | |||
|}; | |||
export default class MyIssuesFilter extends React.Component { | |||
props: Props; | |||
handleClick = (myIssues: boolean) => | |||
(e: Event & { currentTarget: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
this.props.onMyIssuesChange(myIssues); | |||
}; | |||
render() { | |||
const { myIssues } = this.props; | |||
return ( | |||
<div className={css({ marginBottom: 24, textAlign: 'center' })}> | |||
<div className="button-group"> | |||
<button | |||
className={myIssues ? 'button-active' : undefined} | |||
onClick={this.handleClick(true)}> | |||
{translate('issues.my_issues')} | |||
</button> | |||
<button | |||
className={myIssues ? undefined : 'button-active'} | |||
onClick={this.handleClick(false)}> | |||
{translate('all')} | |||
</button> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,77 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import type { Paging } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
type Props = {| | |||
loading: boolean, | |||
openIssue: ?{}, | |||
paging: ?Paging, | |||
selectedIndex: ?number | |||
|}; | |||
export default class PageActions extends React.Component { | |||
props: Props; | |||
renderShortcuts() { | |||
return ( | |||
<span className="note big-spacer-right"> | |||
<span className="big-spacer-right"> | |||
<span className="shortcut-button little-spacer-right">↑</span> | |||
<span className="shortcut-button little-spacer-right">↓</span> | |||
{translate('issues.to_select_issues')} | |||
</span> | |||
<span> | |||
<span className="shortcut-button little-spacer-right">←</span> | |||
<span className="shortcut-button little-spacer-right">→</span> | |||
{translate('issues.to_navigate')} | |||
</span> | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { openIssue, paging, selectedIndex } = this.props; | |||
return ( | |||
<div className={css({ float: 'right' })}> | |||
{openIssue == null && this.renderShortcuts()} | |||
<div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}> | |||
{this.props.loading && <i className="spinner spacer-right" />} | |||
{paging != null && | |||
<span> | |||
<strong> | |||
{selectedIndex != null && <span>{selectedIndex + 1} / </span>} | |||
{formatMeasure(paging.total, 'INT')} | |||
</strong> | |||
{' '} | |||
{translate('issues.issues')} | |||
</span>} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,122 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { debounce } from 'lodash'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
type Option = { label: string, value: string }; | |||
type Props = {| | |||
minimumQueryLength: number, | |||
onSearch: (query: string) => Promise<Array<Option>>, | |||
onSelect: (value: string) => void, | |||
renderOption?: (option: Object) => React.Element<*>, | |||
resetOnBlur: boolean, | |||
value?: string | |||
|}; | |||
type State = { | |||
loading: boolean, | |||
options: Array<Option>, | |||
query: string | |||
}; | |||
export default class SearchSelect extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
static defaultProps = { | |||
minimumQueryLength: 2, | |||
resetOnBlur: true | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { loading: false, options: [], query: '' }; | |||
this.search = debounce(this.search, 250); | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
search = (query: string) => { | |||
this.props.onSearch(query).then(options => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, options }); | |||
} | |||
}); | |||
}; | |||
handleBlur = () => { | |||
this.setState({ options: [], query: '' }); | |||
}; | |||
handleChange = (option: Option) => { | |||
this.props.onSelect(option.value); | |||
}; | |||
handleInputChange = (query: string = '') => { | |||
if (query.length >= this.props.minimumQueryLength) { | |||
this.setState({ loading: true, query }); | |||
this.search(query); | |||
} else { | |||
this.setState({ options: [], query }); | |||
} | |||
}; | |||
// disable internal filtering | |||
handleFilterOption = () => true; | |||
render() { | |||
return ( | |||
<Select | |||
autofocus={true} | |||
cache={false} | |||
className="input-super-large" | |||
clearable={false} | |||
filterOption={this.handleFilterOption} | |||
isLoading={this.state.loading} | |||
noResultsText={ | |||
this.state.query.length < this.props.minimumQueryLength | |||
? translateWithParameters('select2.tooShort', this.props.minimumQueryLength) | |||
: translate('select2.noMatches') | |||
} | |||
onBlur={this.props.resetOnBlur ? this.handleBlur : undefined} | |||
onChange={this.handleChange} | |||
onInputChange={this.handleInputChange} | |||
onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined} | |||
optionRenderer={this.props.renderOption} | |||
options={this.state.options} | |||
placeholder={translate('search_verb')} | |||
searchable={true} | |||
value={this.props.value} | |||
valueRenderer={this.props.renderOption} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import SearchSelect from '../SearchSelect'; | |||
jest.mock('lodash', () => ({ | |||
debounce: fn => fn | |||
})); | |||
it('should render Select', () => { | |||
expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot(); | |||
}); | |||
it('should call onSelect', () => { | |||
const onSelect = jest.fn(); | |||
const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />); | |||
wrapper.prop('onChange')({ value: 'foo' }); | |||
expect(onSelect).lastCalledWith('foo'); | |||
}); | |||
it('should call onSearch', () => { | |||
const onSearch = jest.fn().mockReturnValue(Promise.resolve([])); | |||
const wrapper = shallow( | |||
<SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} /> | |||
); | |||
wrapper.prop('onInputChange')('f'); | |||
expect(onSearch).not.toHaveBeenCalled(); | |||
wrapper.prop('onInputChange')('foo'); | |||
expect(onSearch).lastCalledWith('foo'); | |||
}); |
@@ -0,0 +1,48 @@ | |||
exports[`test should render Select 1`] = ` | |||
<Select | |||
addLabelText="Add \"{label}\"?" | |||
arrowRenderer={[Function]} | |||
autofocus={true} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
cache={false} | |||
className="input-super-large" | |||
clearAllText="Clear all" | |||
clearValueText="Clear value" | |||
clearable={false} | |||
delimiter="," | |||
disabled={false} | |||
escapeClearsValue={true} | |||
filterOption={[Function]} | |||
filterOptions={[Function]} | |||
ignoreAccents={true} | |||
ignoreCase={true} | |||
inputProps={Object {}} | |||
isLoading={false} | |||
joinValues={false} | |||
labelKey="label" | |||
matchPos="any" | |||
matchProp="any" | |||
menuBuffer={0} | |||
menuRenderer={[Function]} | |||
multi={false} | |||
noResultsText="select2.tooShort.2" | |||
onBlur={[Function]} | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
onInputChange={[Function]} | |||
openAfterFocus={false} | |||
optionComponent={[Function]} | |||
options={Array []} | |||
pageSize={5} | |||
placeholder="search_verb" | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={true} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
valueComponent={[Function]} | |||
valueKey="value" /> | |||
`; |
@@ -1,217 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import Backbone from 'backbone'; | |||
import Controller from '../../components/navigator/controller'; | |||
import ComponentViewer from './component-viewer/main'; | |||
import getStore from '../../app/utils/getStore'; | |||
import { receiveIssues } from '../../store/issues/duck'; | |||
const FACET_DATA_FIELDS = ['components', 'users', 'rules', 'languages']; | |||
export default Controller.extend({ | |||
_issuesParameters() { | |||
return { | |||
p: this.options.app.state.get('page'), | |||
ps: this.pageSize, | |||
asc: true, | |||
additionalFields: '_all', | |||
facets: this._facetsFromServer().join() | |||
}; | |||
}, | |||
receiveIssues(issues) { | |||
const store = getStore(); | |||
store.dispatch(receiveIssues(issues)); | |||
}, | |||
fetchList(firstPage) { | |||
const that = this; | |||
if (firstPage == null) { | |||
firstPage = true; | |||
} | |||
if (firstPage) { | |||
this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true }); | |||
this.closeComponentViewer(); | |||
} | |||
const data = this._issuesParameters(); | |||
Object.assign(data, this.options.app.state.get('query')); | |||
if (this.options.app.state.get('query').assigned_to_me) { | |||
Object.assign(data, { assignees: '__me__' }); | |||
} | |||
if (this.options.app.state.get('isContext')) { | |||
Object.assign(data, this.options.app.state.get('contextQuery')); | |||
} | |||
return $.get(window.baseUrl + '/api/issues/search', data).done(r => { | |||
const issues = that.options.app.list.parseIssues(r); | |||
this.receiveIssues(issues); | |||
if (firstPage) { | |||
const issues = that.options.app.list.parseIssues(r); | |||
that.options.app.list.reset(issues); | |||
} else { | |||
const issues = that.options.app.list.parseIssues(r, that.options.app.list.length); | |||
that.options.app.list.add(issues); | |||
} | |||
that.options.app.list.setIndex(); | |||
FACET_DATA_FIELDS.forEach(field => { | |||
that.options.app.facets[field] = r[field]; | |||
}); | |||
that.options.app.facets.reset(that._allFacets()); | |||
that.options.app.facets.add(r.facets, { merge: true }); | |||
that.enableFacets(that._enabledFacets()); | |||
if (firstPage) { | |||
that.options.app.state.set({ | |||
page: r.p, | |||
pageSize: r.ps, | |||
total: r.total, | |||
maxResultsReached: r.p * r.ps >= r.total | |||
}); | |||
} else { | |||
that.options.app.state.set({ | |||
page: r.p, | |||
maxResultsReached: r.p * r.ps >= r.total | |||
}); | |||
} | |||
if (firstPage && that.isIssuePermalink()) { | |||
that.showComponentViewer(that.options.app.list.first()); | |||
} | |||
}); | |||
}, | |||
isIssuePermalink() { | |||
const query = this.options.app.state.get('query'); | |||
return query.issues != null && this.options.app.list.length === 1; | |||
}, | |||
_mergeCollections(a, b) { | |||
const collection = new Backbone.Collection(a); | |||
collection.add(b, { merge: true }); | |||
return collection.toJSON(); | |||
}, | |||
requestFacet(id) { | |||
const that = this; | |||
const facet = this.options.app.facets.get(id); | |||
const data = { | |||
facets: id, | |||
ps: 1, | |||
additionalFields: '_all', | |||
...this.options.app.state.get('query') | |||
}; | |||
if (this.options.app.state.get('query').assigned_to_me) { | |||
Object.assign(data, { assignees: '__me__' }); | |||
} | |||
if (this.options.app.state.get('isContext')) { | |||
Object.assign(data, this.options.app.state.get('contextQuery')); | |||
} | |||
return $.get(window.baseUrl + '/api/issues/search', data, r => { | |||
FACET_DATA_FIELDS.forEach(field => { | |||
that.options.app.facets[field] = that._mergeCollections( | |||
that.options.app.facets[field], | |||
r[field] | |||
); | |||
}); | |||
const facetData = r.facets.find(facet => facet.property === id); | |||
if (facetData != null) { | |||
facet.set(facetData); | |||
} | |||
}); | |||
}, | |||
newSearch() { | |||
this.options.app.state.unset('filter'); | |||
return this.options.app.state.setQuery({ resolved: 'false' }); | |||
}, | |||
parseQuery() { | |||
const q = Controller.prototype.parseQuery.apply(this, arguments); | |||
delete q.asc; | |||
delete q.s; | |||
delete q.id; | |||
return q; | |||
}, | |||
getQueryAsObject() { | |||
const state = this.options.app.state; | |||
const query = state.get('query'); | |||
if (query.assigned_to_me) { | |||
Object.assign(query, { assignees: '__me__' }); | |||
} | |||
if (state.get('isContext')) { | |||
Object.assign(query, state.get('contextQuery')); | |||
} | |||
return query; | |||
}, | |||
getQuery(separator, addContext, handleMyIssues = false) { | |||
if (separator == null) { | |||
separator = '|'; | |||
} | |||
if (addContext == null) { | |||
addContext = false; | |||
} | |||
const filter = this.options.app.state.get('query'); | |||
if (addContext && this.options.app.state.get('isContext')) { | |||
Object.assign(filter, this.options.app.state.get('contextQuery')); | |||
} | |||
if (handleMyIssues && this.options.app.state.get('query').assigned_to_me) { | |||
Object.assign(filter, { assignees: '__me__' }); | |||
} | |||
const route = []; | |||
Object.keys(filter).forEach(property => { | |||
route.push(`${property}=${encodeURIComponent(filter[property])}`); | |||
}); | |||
return route.join(separator); | |||
}, | |||
_prepareComponent(issue) { | |||
return { | |||
key: issue.get('component'), | |||
name: issue.get('componentLongName'), | |||
qualifier: issue.get('componentQualifier'), | |||
subProject: issue.get('subProject'), | |||
subProjectName: issue.get('subProjectLongName'), | |||
project: issue.get('project'), | |||
projectName: issue.get('projectLongName'), | |||
projectOrganization: issue.get('projectOrganization') | |||
}; | |||
}, | |||
showComponentViewer(issue) { | |||
this.options.app.layout.workspaceComponentViewerRegion.reset(); | |||
key.setScope('componentViewer'); | |||
this.options.app.issuesView.unbindScrollEvents(); | |||
this.options.app.state.set('component', this._prepareComponent(issue)); | |||
this.options.app.componentViewer = new ComponentViewer({ app: this.options.app }); | |||
this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer); | |||
this.options.app.layout.showComponentViewer(); | |||
return this.options.app.componentViewer.openFileByIssue(issue); | |||
}, | |||
closeComponentViewer() { | |||
key.setScope('list'); | |||
$('body').click(); | |||
this.options.app.state.unset('component'); | |||
this.options.app.layout.workspaceComponentViewerRegion.reset(); | |||
this.options.app.layout.hideComponentViewer(); | |||
this.options.app.issuesView.bindScrollEvents(); | |||
return this.options.app.issuesView.scrollTo(); | |||
} | |||
}); |
@@ -1,63 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import FacetsView from '../../components/navigator/facets-view'; | |||
import BaseFacet from './facets/base-facet'; | |||
import TypeFacet from './facets/type-facet'; | |||
import SeverityFacet from './facets/severity-facet'; | |||
import StatusFacet from './facets/status-facet'; | |||
import ProjectFacet from './facets/project-facet'; | |||
import ModuleFacet from './facets/module-facet'; | |||
import AssigneeFacet from './facets/assignee-facet'; | |||
import RuleFacet from './facets/rule-facet'; | |||
import TagFacet from './facets/tag-facet'; | |||
import ResolutionFacet from './facets/resolution-facet'; | |||
import CreationDateFacet from './facets/creation-date-facet'; | |||
import FileFacet from './facets/file-facet'; | |||
import LanguageFacet from './facets/language-facet'; | |||
import AuthorFacet from './facets/author-facet'; | |||
import IssueKeyFacet from './facets/issue-key-facet'; | |||
import ContextFacet from './facets/context-facet'; | |||
import ModeFacet from './facets/mode-facet'; | |||
const viewsMapping = { | |||
types: TypeFacet, | |||
severities: SeverityFacet, | |||
statuses: StatusFacet, | |||
assignees: AssigneeFacet, | |||
resolutions: ResolutionFacet, | |||
createdAt: CreationDateFacet, | |||
projectUuids: ProjectFacet, | |||
moduleUuids: ModuleFacet, | |||
rules: RuleFacet, | |||
tags: TagFacet, | |||
fileUuids: FileFacet, | |||
languages: LanguageFacet, | |||
authors: AuthorFacet, | |||
issues: IssueKeyFacet, | |||
context: ContextFacet, | |||
facetMode: ModeFacet | |||
}; | |||
export default FacetsView.extend({ | |||
getChildView(model) { | |||
const view = viewsMapping[model.get('property')]; | |||
return view ? view : BaseFacet; | |||
} | |||
}); |
@@ -1,135 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import { sortBy } from 'lodash'; | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import Template from '../templates/facets/issues-assignee-facet.hbs'; | |||
export default CustomValuesFacet.extend({ | |||
template: Template, | |||
initialize() { | |||
this.context = { | |||
isContext: this.options.app.state.get('isContext'), | |||
organization: this.options.app.state.get('contextOrganization') | |||
}; | |||
}, | |||
getUrl() { | |||
return window.baseUrl + | |||
(this.context.isContext ? '/api/organizations/search_members' : '/api/users/search'); | |||
}, | |||
prepareAjaxSearch() { | |||
return { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data: (term, page) => { | |||
if (this.context.isContext && this.context.organization) { | |||
return { q: term, p: page, organization: this.context.organization }; | |||
} else { | |||
return { q: term, p: page }; | |||
} | |||
}, | |||
results: window.usersToSelect2 | |||
}; | |||
}, | |||
onRender() { | |||
CustomValuesFacet.prototype.onRender.apply(this, arguments); | |||
const myIssuesSelected = !!this.options.app.state.get('query').assigned_to_me; | |||
this.$el.toggleClass('hidden', myIssuesSelected); | |||
const value = this.options.app.state.get('query').assigned; | |||
if (value != null && (!value || value === 'false')) { | |||
this.$('.js-facet').filter('[data-unassigned]').addClass('active'); | |||
} | |||
}, | |||
toggleFacet(e) { | |||
const unassigned = $(e.currentTarget).is('[data-unassigned]'); | |||
$(e.currentTarget).toggleClass('active'); | |||
if (unassigned) { | |||
const checked = $(e.currentTarget).is('.active'); | |||
const value = checked ? 'false' : null; | |||
return this.options.app.state.updateFilter({ | |||
assigned: value, | |||
assignees: null, | |||
assigned_to_me: null | |||
}); | |||
} else { | |||
return this.options.app.state.updateFilter({ | |||
assigned: null, | |||
assignees: this.getValue(), | |||
assigned_to_me: null | |||
}); | |||
} | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const users = this.options.app.facets.users; | |||
values.forEach(v => { | |||
const login = v.val; | |||
let name = ''; | |||
if (login) { | |||
const user = users.find(user => user.login === login); | |||
if (user != null) { | |||
name = user.name; | |||
} | |||
} | |||
v.label = name; | |||
}); | |||
return values; | |||
}, | |||
disable() { | |||
return this.options.app.state.updateFilter({ | |||
assigned: null, | |||
assignees: null | |||
}); | |||
}, | |||
addCustomValue() { | |||
const property = this.model.get('property'); | |||
const customValue = this.$('.js-custom-value').select2('val'); | |||
let value = this.getValue(); | |||
if (value.length > 0) { | |||
value += ','; | |||
} | |||
value += customValue; | |||
const obj = {}; | |||
obj[property] = value; | |||
obj.assigned = null; | |||
return this.options.app.state.updateFilter(obj); | |||
}, | |||
sortValues(values) { | |||
return sortBy(values, v => v.val === '' ? -999999 : -v.count); | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,60 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default CustomValuesFacet.extend({ | |||
getUrl() { | |||
return window.baseUrl + '/api/issues/authors'; | |||
}, | |||
prepareSearch() { | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 2, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
formatInputTooShort() { | |||
return translateWithParameters('select2.tooShort', 2); | |||
}, | |||
width: '100%', | |||
ajax: { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data(term) { | |||
return { q: term, ps: 25 }; | |||
}, | |||
results(data) { | |||
return { | |||
more: false, | |||
results: data.authors.map(author => { | |||
return { id: author, text: author }; | |||
}) | |||
}; | |||
} | |||
} | |||
}); | |||
} | |||
}); |
@@ -1,41 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import BaseFacet from '../../../components/navigator/facets/base-facet'; | |||
import Template from '../templates/facets/issues-base-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
onRender() { | |||
BaseFacet.prototype.onRender.apply(this, arguments); | |||
return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); | |||
}, | |||
onDestroy() { | |||
return this.$('[data-toggle="tooltip"]').tooltip('destroy'); | |||
}, | |||
serializeData() { | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
state: this.options.app.state.toJSON() | |||
}; | |||
} | |||
}); |
@@ -1,32 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-context-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
serializeData() { | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
state: this.options.app.state.toJSON() | |||
}; | |||
} | |||
}); |
@@ -1,176 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import moment from 'moment'; | |||
import { times } from 'lodash'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-creation-date-facet.hbs'; | |||
import '../../../components/widgets/barchart'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
events() { | |||
return { | |||
...BaseFacet.prototype.events.apply(this, arguments), | |||
'change input': 'applyFacet', | |||
'click .js-select-period-start': 'selectPeriodStart', | |||
'click .js-select-period-end': 'selectPeriodEnd', | |||
'click .sonar-d3 rect': 'selectBar', | |||
'click .js-all': 'onAllClick', | |||
'click .js-last-week': 'onLastWeekClick', | |||
'click .js-last-month': 'onLastMonthClick', | |||
'click .js-last-year': 'onLastYearClick', | |||
'click .js-leak': 'onLeakClick' | |||
}; | |||
}, | |||
onRender() { | |||
const that = this; | |||
this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled')); | |||
this.$('input').datepicker({ | |||
dateFormat: 'yy-mm-dd', | |||
changeMonth: true, | |||
changeYear: true | |||
}); | |||
const props = ['createdAfter', 'createdBefore', 'createdAt']; | |||
const query = this.options.app.state.get('query'); | |||
props.forEach(prop => { | |||
const value = query[prop]; | |||
if (value != null) { | |||
that.$(`input[name=${prop}]`).val(value); | |||
} | |||
}); | |||
let values = this.model.getValues(); | |||
if (!(Array.isArray(values) && values.length > 0)) { | |||
let date = moment(); | |||
values = []; | |||
times(10, () => { | |||
values.push({ count: 0, val: date.toDate().toString() }); | |||
date = date.subtract(1, 'days'); | |||
}); | |||
values.reverse(); | |||
} | |||
values = values.map(v => { | |||
const format = that.options.app.state.getFacetMode() === 'count' | |||
? 'SHORT_INT' | |||
: 'SHORT_WORK_DUR'; | |||
const text = formatMeasure(v.count, format); | |||
return { ...v, text }; | |||
}); | |||
return this.$('.js-barchart').barchart(values); | |||
}, | |||
selectPeriodStart() { | |||
return this.$('.js-period-start').datepicker('show'); | |||
}, | |||
selectPeriodEnd() { | |||
return this.$('.js-period-end').datepicker('show'); | |||
}, | |||
applyFacet() { | |||
const obj = { createdAt: null, createdInLast: null }; | |||
this.$('input').each(function() { | |||
const property = $(this).prop('name'); | |||
const value = $(this).val(); | |||
obj[property] = value; | |||
}); | |||
return this.options.app.state.updateFilter(obj); | |||
}, | |||
disable() { | |||
return this.options.app.state.updateFilter({ | |||
createdAfter: null, | |||
createdBefore: null, | |||
createdAt: null, | |||
sinceLeakPeriod: null, | |||
createdInLast: null | |||
}); | |||
}, | |||
selectBar(e) { | |||
const periodStart = $(e.currentTarget).data('period-start'); | |||
const periodEnd = $(e.currentTarget).data('period-end'); | |||
return this.options.app.state.updateFilter({ | |||
createdAfter: periodStart, | |||
createdBefore: periodEnd, | |||
createdAt: null, | |||
sinceLeakPeriod: null, | |||
createdInLast: null | |||
}); | |||
}, | |||
selectPeriod(period) { | |||
return this.options.app.state.updateFilter({ | |||
createdAfter: null, | |||
createdBefore: null, | |||
createdAt: null, | |||
sinceLeakPeriod: null, | |||
createdInLast: period | |||
}); | |||
}, | |||
onAllClick(e) { | |||
e.preventDefault(); | |||
return this.disable(); | |||
}, | |||
onLastWeekClick(e) { | |||
e.preventDefault(); | |||
return this.selectPeriod('1w'); | |||
}, | |||
onLastMonthClick(e) { | |||
e.preventDefault(); | |||
return this.selectPeriod('1m'); | |||
}, | |||
onLastYearClick(e) { | |||
e.preventDefault(); | |||
return this.selectPeriod('1y'); | |||
}, | |||
onLeakClick(e) { | |||
e.preventDefault(); | |||
this.options.app.state.updateFilter({ | |||
createdAfter: null, | |||
createdBefore: null, | |||
createdAt: null, | |||
createdInLast: null, | |||
sinceLeakPeriod: 'true' | |||
}); | |||
}, | |||
serializeData() { | |||
const hasLeak = this.options.app.state.get('contextComponentQualifier') === 'TRK'; | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
hasLeak, | |||
periodStart: this.options.app.state.get('query').createdAfter, | |||
periodEnd: this.options.app.state.get('query').createdBefore, | |||
createdAt: this.options.app.state.get('query').createdAt, | |||
sinceLeakPeriod: this.options.app.state.get('query').sinceLeakPeriod, | |||
createdInLast: this.options.app.state.get('query').createdInLast | |||
}; | |||
} | |||
}); |
@@ -1,85 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-custom-values-facet.hbs'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
events() { | |||
return { | |||
...BaseFacet.prototype.events.apply(this, arguments), | |||
'change .js-custom-value': 'addCustomValue' | |||
}; | |||
}, | |||
getUrl() {}, | |||
onRender() { | |||
BaseFacet.prototype.onRender.apply(this, arguments); | |||
return this.prepareSearch(); | |||
}, | |||
prepareSearch() { | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 2, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
formatInputTooShort() { | |||
return translateWithParameters('select2.tooShort', 2); | |||
}, | |||
width: '100%', | |||
ajax: this.prepareAjaxSearch() | |||
}); | |||
}, | |||
prepareAjaxSearch() { | |||
return { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data(term, page) { | |||
return { s: term, p: page }; | |||
}, | |||
results(data) { | |||
return { more: data.more, results: data.results }; | |||
} | |||
}; | |||
}, | |||
addCustomValue() { | |||
const property = this.model.get('property'); | |||
const customValue = this.$('.js-custom-value').select2('val'); | |||
let value = this.getValue(); | |||
if (value.length > 0) { | |||
value += ','; | |||
} | |||
value += customValue; | |||
const obj = {}; | |||
obj[property] = value; | |||
return this.options.app.state.updateFilter(obj); | |||
} | |||
}); |
@@ -1,61 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-file-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
onRender() { | |||
BaseFacet.prototype.onRender.apply(this, arguments); | |||
const widths = this.$('.facet-stat') | |||
.map(function() { | |||
return $(this).outerWidth(); | |||
}) | |||
.get(); | |||
const maxValueWidth = Math.max(...widths); | |||
return this.$('.facet-name').css('padding-right', maxValueWidth); | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const source = this.options.app.facets.components; | |||
values.forEach(v => { | |||
const key = v.val; | |||
let label = null; | |||
if (key) { | |||
const item = source.find(file => file.uuid === key); | |||
if (item != null) { | |||
label = item.longName; | |||
} | |||
} | |||
v.label = label; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,40 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-issue-key-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
onRender() { | |||
return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues); | |||
}, | |||
disable() { | |||
return this.options.app.state.updateFilter({ issues: null }); | |||
}, | |||
serializeData() { | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
issues: this.options.app.state.get('query').issues | |||
}; | |||
} | |||
}); |
@@ -1,84 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default CustomValuesFacet.extend({ | |||
getUrl() { | |||
return window.baseUrl + '/api/languages/list'; | |||
}, | |||
prepareSearch() { | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 2, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
formatInputTooShort() { | |||
return translateWithParameters('select2.tooShort', 2); | |||
}, | |||
width: '100%', | |||
ajax: { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data(term) { | |||
return { q: term, ps: 0 }; | |||
}, | |||
results(data) { | |||
return { | |||
more: false, | |||
results: data.languages.map(lang => { | |||
return { id: lang.key, text: lang.name }; | |||
}) | |||
}; | |||
} | |||
} | |||
}); | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const source = this.options.app.facets.languages; | |||
values.forEach(v => { | |||
const key = v.val; | |||
let label = null; | |||
if (key) { | |||
const item = source.find(lang => lang.key === key); | |||
if (item != null) { | |||
label = item.name; | |||
} | |||
} | |||
v.label = label; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,46 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import BaseFacet from './base-facet'; | |||
export default BaseFacet.extend({ | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const components = this.options.app.facets.components; | |||
values.forEach(v => { | |||
const uuid = v.val; | |||
let label = uuid; | |||
if (uuid) { | |||
const component = components.find(c => c.uuid === uuid); | |||
if (component != null) { | |||
label = component.longName; | |||
} | |||
} | |||
v.label = label; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...BaseFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,112 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import Template from '../templates/facets/issues-projects-facet.hbs'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { areThereCustomOrganizations, getOrganization } from '../../../store/organizations/utils'; | |||
export default CustomValuesFacet.extend({ | |||
template: Template, | |||
getUrl() { | |||
return window.baseUrl + '/api/components/search'; | |||
}, | |||
prepareSearchForViews() { | |||
const contextId = this.options.app.state.get('contextComponentUuid'); | |||
return { | |||
url: window.baseUrl + '/api/components/tree', | |||
data(term, page) { | |||
return { q: term, p: page, qualifiers: 'TRK', baseComponentId: contextId }; | |||
} | |||
}; | |||
}, | |||
prepareAjaxSearch() { | |||
const options = { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data(term, page) { | |||
return { q: term, p: page, qualifiers: 'TRK' }; | |||
}, | |||
results: r => ({ | |||
more: r.paging.total > r.paging.pageIndex * r.paging.pageSize, | |||
results: r.components.map(component => ({ | |||
id: component.id, | |||
text: component.name | |||
})) | |||
}) | |||
}; | |||
const contextQualifier = this.options.app.state.get('contextComponentQualifier'); | |||
if (contextQualifier === 'VW' || contextQualifier === 'SVW') { | |||
Object.assign(options, this.prepareSearchForViews()); | |||
} | |||
return options; | |||
}, | |||
prepareSearch() { | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 3, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
formatInputTooShort() { | |||
return translateWithParameters('select2.tooShort', 3); | |||
}, | |||
width: '100%', | |||
ajax: this.prepareAjaxSearch() | |||
}); | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const projects = this.options.app.facets.components; | |||
const displayOrganizations = areThereCustomOrganizations(); | |||
values.forEach(v => { | |||
const uuid = v.val; | |||
let label = ''; | |||
let organization = null; | |||
if (uuid) { | |||
const project = projects.find(p => p.uuid === uuid); | |||
if (project != null) { | |||
label = project.longName; | |||
organization = displayOrganizations && project.organization | |||
? getOrganization(project.organization) | |||
: null; | |||
} | |||
} | |||
v.label = label; | |||
v.organization = organization; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,61 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
export default CustomValuesFacet.extend({ | |||
getUrl() { | |||
return window.baseUrl + '/api/users/search'; | |||
}, | |||
prepareAjaxSearch() { | |||
return { | |||
quietMillis: 300, | |||
url: this.getUrl(), | |||
data(term, page) { | |||
return { q: term, p: page }; | |||
}, | |||
results: window.usersToSelect2 | |||
}; | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const source = this.options.app.facets.users; | |||
values.forEach(v => { | |||
const key = v.val; | |||
let label = null; | |||
if (key) { | |||
const item = source.find(user => user.login === key); | |||
if (item != null) { | |||
label = item.name; | |||
} | |||
} | |||
v.label = label; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,65 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import { sortBy } from 'lodash'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-resolution-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
onRender() { | |||
BaseFacet.prototype.onRender.apply(this, arguments); | |||
const value = this.options.app.state.get('query').resolved; | |||
if (value != null && (!value || value === 'false')) { | |||
this.$('.js-facet').filter('[data-unresolved]').addClass('active'); | |||
} | |||
}, | |||
toggleFacet(e) { | |||
const unresolved = $(e.currentTarget).is('[data-unresolved]'); | |||
$(e.currentTarget).toggleClass('active'); | |||
if (unresolved) { | |||
const checked = $(e.currentTarget).is('.active'); | |||
const value = checked ? 'false' : null; | |||
return this.options.app.state.updateFilter({ | |||
resolved: value, | |||
resolutions: null | |||
}); | |||
} else { | |||
return this.options.app.state.updateFilter({ | |||
resolved: null, | |||
resolutions: this.getValue() | |||
}); | |||
} | |||
}, | |||
disable() { | |||
return this.options.app.state.updateFilter({ | |||
resolved: null, | |||
resolutions: null | |||
}); | |||
}, | |||
sortValues(values) { | |||
const order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED']; | |||
return sortBy(values, v => order.indexOf(v.val)); | |||
} | |||
}); |
@@ -1,95 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default CustomValuesFacet.extend({ | |||
prepareSearch() { | |||
let url = window.baseUrl + '/api/rules/search?f=name,langName'; | |||
const languages = this.options.app.state.get('query').languages; | |||
if (languages != null) { | |||
url += '&languages=' + languages; | |||
} | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 2, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
formatInputTooShort() { | |||
return translateWithParameters('select2.tooShort', 2); | |||
}, | |||
width: '100%', | |||
ajax: { | |||
url, | |||
quietMillis: 300, | |||
data(term, page) { | |||
return { q: term, p: page }; | |||
}, | |||
results(data) { | |||
const results = data.rules.map(rule => { | |||
const lang = rule.langName || translate('manual'); | |||
return { | |||
id: rule.key, | |||
text: '(' + lang + ') ' + rule.name | |||
}; | |||
}); | |||
return { | |||
more: data.p * data.ps < data.total, | |||
results | |||
}; | |||
} | |||
} | |||
}); | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
const rules = this.options.app.facets.rules; | |||
values.forEach(v => { | |||
const key = v.val; | |||
let label = ''; | |||
let extra = ''; | |||
if (key) { | |||
const rule = rules.find(r => r.key === key); | |||
if (rule != null) { | |||
label = rule.name; | |||
} | |||
if (rule != null) { | |||
extra = rule.langName; | |||
} | |||
} | |||
v.label = label; | |||
v.extra = extra; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,31 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { sortBy } from 'lodash'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-severity-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
sortValues(values) { | |||
const order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR']; | |||
return sortBy(values, v => order.indexOf(v.val)); | |||
} | |||
}); |
@@ -1,31 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { sortBy } from 'lodash'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-status-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
sortValues(values) { | |||
const order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED']; | |||
return sortBy(values, v => order.indexOf(v.val)); | |||
} | |||
}); |
@@ -1,72 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import CustomValuesFacet from './custom-values-facet'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default CustomValuesFacet.extend({ | |||
prepareSearch() { | |||
let url = window.baseUrl + '/api/issues/tags?ps=10'; | |||
const tags = this.options.app.state.get('query').tags; | |||
if (tags != null) { | |||
url += '&tags=' + tags; | |||
} | |||
return this.$('.js-custom-value').select2({ | |||
placeholder: translate('search_verb'), | |||
minimumInputLength: 0, | |||
allowClear: false, | |||
formatNoMatches() { | |||
return translate('select2.noMatches'); | |||
}, | |||
formatSearching() { | |||
return translate('select2.searching'); | |||
}, | |||
width: '100%', | |||
ajax: { | |||
url, | |||
quietMillis: 300, | |||
data(term) { | |||
return { q: term, ps: 10 }; | |||
}, | |||
results(data) { | |||
const results = data.tags.map(tag => { | |||
return { id: tag, text: tag }; | |||
}); | |||
return { more: false, results }; | |||
} | |||
} | |||
}); | |||
}, | |||
getValuesWithLabels() { | |||
const values = this.model.getValues(); | |||
values.forEach(v => { | |||
v.label = v.val; | |||
v.extra = ''; | |||
}); | |||
return values; | |||
}, | |||
serializeData() { | |||
return { | |||
...CustomValuesFacet.prototype.serializeData.apply(this, arguments), | |||
values: this.sortValues(this.getValuesWithLabels()) | |||
}; | |||
} | |||
}); |
@@ -1,31 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { sortBy } from 'lodash'; | |||
import BaseFacet from './base-facet'; | |||
import Template from '../templates/facets/issues-type-facet.hbs'; | |||
export default BaseFacet.extend({ | |||
template: Template, | |||
sortValues(values) { | |||
const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; | |||
return sortBy(values, v => order.indexOf(v.val)); | |||
} | |||
}); |
@@ -1,87 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import Backbone from 'backbone'; | |||
import Marionette from 'backbone.marionette'; | |||
import State from './models/state'; | |||
import Layout from './layout'; | |||
import Issues from './models/issues'; | |||
import Facets from '../../components/navigator/models/facets'; | |||
import Controller from './controller'; | |||
import Router from './router'; | |||
import WorkspaceListView from './workspace-list-view'; | |||
import WorkspaceHeaderView from './workspace-header-view'; | |||
import FacetsView from './facets-view'; | |||
import HeaderView from './HeaderView'; | |||
const App = new Marionette.Application(); | |||
const init = function({ el, user }) { | |||
this.state = new State({ user, canBulkChange: user.isLoggedIn }); | |||
this.list = new Issues(); | |||
this.facets = new Facets(); | |||
this.layout = new Layout({ app: this, el }); | |||
this.layout.render(); | |||
$('#footer').addClass('search-navigator-footer'); | |||
this.controller = new Controller({ app: this }); | |||
this.issuesView = new WorkspaceListView({ | |||
app: this, | |||
collection: this.list | |||
}); | |||
this.layout.workspaceListRegion.show(this.issuesView); | |||
this.issuesView.bindScrollEvents(); | |||
this.workspaceHeaderView = new WorkspaceHeaderView({ | |||
app: this, | |||
collection: this.list | |||
}); | |||
this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView); | |||
this.facetsView = new FacetsView({ | |||
app: this, | |||
collection: this.facets | |||
}); | |||
this.layout.facetsRegion.show(this.facetsView); | |||
this.headerView = new HeaderView({ | |||
app: this | |||
}); | |||
this.layout.filtersRegion.show(this.headerView); | |||
key.setScope('list'); | |||
App.router = new Router({ app: App }); | |||
Backbone.history.start(); | |||
}; | |||
App.on('start', el => { | |||
init.call(App, el); | |||
}); | |||
export default function(el, user) { | |||
App.start({ el, user }); | |||
return () => { | |||
Backbone.history.stop(); | |||
App.layout.destroy(); | |||
$('#footer').removeClass('search-navigator-footer'); | |||
}; | |||
} |
@@ -1,40 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import ActionOptionsView from '../../components/common/action-options-view'; | |||
import Template from './templates/issues-issue-filter-form.hbs'; | |||
export default ActionOptionsView.extend({ | |||
template: Template, | |||
selectOption(e) { | |||
const property = $(e.currentTarget).data('property'); | |||
const value = $(e.currentTarget).data('value'); | |||
this.trigger('select', property, value); | |||
ActionOptionsView.prototype.selectOption.apply(this, arguments); | |||
}, | |||
serializeData() { | |||
return { | |||
...ActionOptionsView.prototype.serializeData.apply(this, arguments), | |||
s: this.model.get('severity') | |||
}; | |||
} | |||
}); |
@@ -1,62 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import Marionette from 'backbone.marionette'; | |||
import Template from './templates/issues-layout.hbs'; | |||
import './styles.css'; | |||
export default Marionette.LayoutView.extend({ | |||
template: Template, | |||
regions: { | |||
filtersRegion: '.issues-header', | |||
facetsRegion: '.search-navigator-facets', | |||
workspaceHeaderRegion: '.search-navigator-workspace-header', | |||
workspaceListRegion: '.search-navigator-workspace-list', | |||
workspaceComponentViewerRegion: '.issues-workspace-component-viewer' | |||
}, | |||
onRender() { | |||
this.$('.search-navigator').addClass('sticky'); | |||
const top = this.$('.search-navigator').offset().top; | |||
this.$('.search-navigator-workspace-header').css({ top }); | |||
this.$('.search-navigator-side').css({ top }).isolatedScroll(); | |||
}, | |||
showSpinner(region) { | |||
return this[region].show( | |||
new Marionette.ItemView({ | |||
template: () => '<i class="spinner"></i>' | |||
}) | |||
); | |||
}, | |||
showComponentViewer() { | |||
this.scroll = $(window).scrollTop(); | |||
this.$('.issues').addClass('issues-extended-view'); | |||
}, | |||
hideComponentViewer() { | |||
this.$('.issues').removeClass('issues-extended-view'); | |||
if (this.scroll != null) { | |||
$(window).scrollTop(this.scroll); | |||
} | |||
} | |||
}); |
@@ -1,30 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import Issue from '../../../components/issue/models/issue'; | |||
export default Issue.extend({ | |||
reset(attrs, options) { | |||
const keepFields = ['index', 'selected', 'comments']; | |||
keepFields.forEach(field => { | |||
attrs[field] = this.get(field); | |||
}); | |||
return Issue.prototype.reset.call(this, attrs, options); | |||
} | |||
}); |
@@ -1,108 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import Backbone from 'backbone'; | |||
import Issue from './issue'; | |||
export default Backbone.Collection.extend({ | |||
model: Issue, | |||
url() { | |||
return window.baseUrl + '/api/issues/search'; | |||
}, | |||
_injectRelational(issue, source, baseField, lookupField) { | |||
const baseValue = issue[baseField]; | |||
if (baseValue != null && Array.isArray(source) && source.length > 0) { | |||
const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); | |||
if (lookupValue != null) { | |||
Object.keys(lookupValue).forEach(key => { | |||
const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1); | |||
issue[newKey] = lookupValue[key]; | |||
}); | |||
} | |||
} | |||
return issue; | |||
}, | |||
_injectCommentsRelational(issue, users) { | |||
if (issue.comments) { | |||
const that = this; | |||
const newComments = issue.comments.map(comment => { | |||
let newComment = { ...comment, author: comment.login }; | |||
delete newComment.login; | |||
newComment = that._injectRelational(newComment, users, 'author', 'login'); | |||
return newComment; | |||
}); | |||
issue = { ...issue, comments: newComments }; | |||
} | |||
return issue; | |||
}, | |||
_prepareClosed(issue) { | |||
if (issue.status === 'CLOSED') { | |||
issue.flows = []; | |||
delete issue.textRange; | |||
} | |||
return issue; | |||
}, | |||
ensureTextRange(issue) { | |||
if (issue.line && !issue.textRange) { | |||
// FIXME 999999 | |||
issue.textRange = { | |||
startLine: issue.line, | |||
endLine: issue.line, | |||
startOffset: 0, | |||
endOffset: 999999 | |||
}; | |||
} | |||
return issue; | |||
}, | |||
parseIssues(r, startIndex = 0) { | |||
const that = this; | |||
return r.issues.map((issue, index) => { | |||
Object.assign(issue, { index: startIndex + index }); | |||
issue = that._injectRelational(issue, r.components, 'component', 'key'); | |||
issue = that._injectRelational(issue, r.components, 'project', 'key'); | |||
issue = that._injectRelational(issue, r.components, 'subProject', 'key'); | |||
issue = that._injectRelational(issue, r.rules, 'rule', 'key'); | |||
issue = that._injectRelational(issue, r.users, 'assignee', 'login'); | |||
issue = that._injectCommentsRelational(issue, r.users); | |||
issue = that._prepareClosed(issue); | |||
issue = that.ensureTextRange(issue); | |||
return issue; | |||
}); | |||
}, | |||
setIndex() { | |||
return this.forEach((issue, index) => issue.set({ index })); | |||
}, | |||
selectByKeys(keys) { | |||
const that = this; | |||
keys.forEach(key => { | |||
const issue = that.get(key); | |||
if (issue) { | |||
issue.set({ selected: true }); | |||
} | |||
}); | |||
} | |||
}); |
@@ -1,81 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import State from '../../../components/navigator/models/state'; | |||
export default State.extend({ | |||
defaults: { | |||
page: 1, | |||
maxResultsReached: false, | |||
query: {}, | |||
facets: ['facetMode', 'types', 'resolutions'], | |||
isContext: false, | |||
allFacets: [ | |||
'facetMode', | |||
'issues', | |||
'types', | |||
'resolutions', | |||
'severities', | |||
'statuses', | |||
'createdAt', | |||
'rules', | |||
'tags', | |||
'projectUuids', | |||
'moduleUuids', | |||
'directories', | |||
'fileUuids', | |||
'assignees', | |||
'authors', | |||
'languages' | |||
], | |||
facetsFromServer: [ | |||
'types', | |||
'severities', | |||
'statuses', | |||
'resolutions', | |||
'projectUuids', | |||
'directories', | |||
'rules', | |||
'moduleUuids', | |||
'tags', | |||
'assignees', | |||
'authors', | |||
'fileUuids', | |||
'languages', | |||
'createdAt' | |||
], | |||
transform: { | |||
resolved: 'resolutions', | |||
assigned: 'assignees', | |||
createdBefore: 'createdAt', | |||
createdAfter: 'createdAt', | |||
sinceLeakPeriod: 'createdAt', | |||
createdInLast: 'createdAt' | |||
} | |||
}, | |||
getFacetMode() { | |||
const query = this.get('query'); | |||
return query.facetMode || 'count'; | |||
}, | |||
toJSON() { | |||
return { facetMode: this.getFacetMode(), ...this.attributes }; | |||
} | |||
}); |
@@ -0,0 +1,55 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils'; | |||
import type { RawQuery } from './utils'; | |||
const parseHash = (hash: string): RawQuery => { | |||
const query: RawQuery = {}; | |||
const parts = hash.split('|'); | |||
parts.forEach(part => { | |||
const tokens = part.split('='); | |||
if (tokens.length === 2) { | |||
const property = decodeURIComponent(tokens[0]); | |||
const value = decodeURIComponent(tokens[1]); | |||
if (property === 'assigned_to_me' && value === 'true') { | |||
query.myIssues = 'true'; | |||
} else { | |||
query[property] = value; | |||
} | |||
} | |||
}); | |||
return query; | |||
}; | |||
export const onEnter = (state: Object, replace: Function) => { | |||
const { hash } = window.location; | |||
if (hash.length > 1) { | |||
const query = parseHash(hash.substr(1)); | |||
const normalizedQuery = { | |||
...serializeQuery(parseQuery(query)), | |||
myIssues: areMyIssuesSelected(query) ? 'true' : undefined | |||
}; | |||
replace({ | |||
pathname: state.location.pathname, | |||
query: normalizedQuery | |||
}); | |||
} | |||
}; |
@@ -1,35 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import Router from '../../components/navigator/router'; | |||
export default Router.extend({ | |||
routes: { | |||
'': 'home', | |||
':query': 'index' | |||
}, | |||
home() { | |||
return this.navigate('resolved=false', { trigger: true, replace: true }); | |||
}, | |||
index(query) { | |||
this.options.app.state.setQuery(this.options.app.controller.parseQuery(query)); | |||
} | |||
}); |
@@ -17,13 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { onEnter } from './redirects'; | |||
const routes = [ | |||
{ | |||
indexRoute: { | |||
getComponent(_, callback) { | |||
require.ensure([], require => | |||
callback(null, require('./components/IssuesAppContainer').default)); | |||
} | |||
getIndexRoute(_, callback) { | |||
require.ensure([], require => | |||
callback(null, { | |||
component: require('./components/AppContainer').default, | |||
onEnter | |||
})); | |||
} | |||
} | |||
]; |
@@ -0,0 +1,168 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, uniq, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import FacetFooter from './components/FacetFooter'; | |||
import { searchAssignees } from '../utils'; | |||
import type { ReferencedUser, Component } from '../utils'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
assigned: boolean, | |||
assignees: Array<string>, | |||
component?: Component, | |||
facetMode: string, | |||
onChange: (changes: {}) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
referencedUsers: { [string]: ReferencedUser } | |||
|}; | |||
export default class AssigneeFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'assignees'; | |||
handleItemClick = (itemValue: string) => { | |||
if (itemValue === '') { | |||
// unassigned | |||
this.props.onChange({ assigned: !this.props.assigned, assignees: [] }); | |||
} else { | |||
// defined assignee | |||
const { assignees } = this.props; | |||
const newValue = sortBy( | |||
assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue] | |||
); | |||
this.props.onChange({ assigned: true, assignees: newValue }); | |||
} | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleSearch = (query: string) => searchAssignees(query, this.props.component); | |||
handleSelect = (assignee: string) => { | |||
const { assignees } = this.props; | |||
this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) }); | |||
}; | |||
isAssigneeActive(assignee: string) { | |||
return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee); | |||
} | |||
getAssigneeName(assignee: string): React.Element<*> | string { | |||
if (assignee === '') { | |||
return translate('unassigned'); | |||
} else { | |||
const { referencedUsers } = this.props; | |||
if (referencedUsers[assignee]) { | |||
return ( | |||
<span> | |||
<Avatar | |||
className="little-spacer-right" | |||
hash={referencedUsers[assignee].avatar} | |||
size={16} | |||
/> | |||
{referencedUsers[assignee].name} | |||
</span> | |||
); | |||
} else { | |||
return assignee; | |||
} | |||
} | |||
} | |||
getStat(assignee: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[assignee] : null; | |||
} | |||
renderOption = (option: { avatar: string, label: string }) => { | |||
return ( | |||
<span> | |||
{option.avatar != null && | |||
<Avatar className="little-spacer-right" hash={option.avatar} size={16} />} | |||
{option.label} | |||
</span> | |||
); | |||
}; | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const assignees = sortBy( | |||
Object.keys(stats), | |||
// put unassigned first | |||
key => key === '' ? 0 : 1, | |||
// the sort by number | |||
key => -stats[key] | |||
); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={!this.props.assigned || this.props.assignees.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{assignees.map(assignee => ( | |||
<FacetItem | |||
active={this.isAssigneeActive(assignee)} | |||
facetMode={this.props.facetMode} | |||
key={assignee} | |||
name={this.getAssigneeName(assignee)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(assignee)} | |||
value={assignee} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && | |||
<FacetFooter | |||
onSearch={this.handleSearch} | |||
onSelect={this.handleSelect} | |||
renderOption={this.renderOption} | |||
/>} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,99 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: {}) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
authors: Array<string> | |||
|}; | |||
export default class AuthorFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'authors'; | |||
handleItemClick = (itemValue: string) => { | |||
const { authors } = this.props; | |||
const newValue = sortBy( | |||
authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getStat(author: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[author] : null; | |||
} | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const authors = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.authors.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{authors.map(author => ( | |||
<FacetItem | |||
active={this.props.authors.includes(author)} | |||
facetMode={this.props.facetMode} | |||
key={author} | |||
name={author} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(author)} | |||
value={author} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,276 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import moment from 'moment'; | |||
import { max } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import { BarChart } from '../../../components/charts/bar-chart'; | |||
import DateInput from '../../../components/controls/DateInput'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import type { Component } from '../utils'; | |||
type Props = {| | |||
component?: Component, | |||
createdAfter: string, | |||
createdAt: string, | |||
createdBefore: string, | |||
createdInLast: string, | |||
facetMode: string, | |||
onChange: (changes: {}) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
sinceLeakPeriod: boolean, | |||
stats?: { [string]: number } | |||
|}; | |||
const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; | |||
export default class CreationDateFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'createdAt'; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
resetTo = (changes: {}) => { | |||
this.props.onChange({ | |||
createdAfter: undefined, | |||
createdAt: undefined, | |||
createdBefore: undefined, | |||
createdInLast: undefined, | |||
sinceLeakPeriod: undefined, | |||
...changes | |||
}); | |||
}; | |||
handleBarClick = ( | |||
{ createdAfter, createdBefore }: { createdAfter: Object, createdBefore?: Object } | |||
) => { | |||
this.resetTo({ | |||
createdAfter: createdAfter.format(DATE_FORMAT), | |||
createdBefore: createdBefore && createdBefore.format(DATE_FORMAT) | |||
}); | |||
}; | |||
handlePeriodChange = (property: string) => | |||
(value: string) => { | |||
this.props.onChange({ | |||
createdAt: undefined, | |||
createdInLast: undefined, | |||
sinceLeakPeriod: undefined, | |||
[property]: value | |||
}); | |||
}; | |||
handlePeriodClick = (period?: string) => | |||
(e: Event & { target: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.target.blur; | |||
this.resetTo({ createdInLast: period }); | |||
}; | |||
handleLeakPeriodClick = () => | |||
(e: Event & { target: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.target.blur; | |||
this.resetTo({ sinceLeakPeriod: true }); | |||
}; | |||
renderBarChart() { | |||
const { createdBefore, stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const periods = Object.keys(stats); | |||
if (periods.length < 2) { | |||
return null; | |||
} | |||
const data = periods.map((startDate, index) => { | |||
const startMoment = moment(startDate); | |||
const nextStartMoment = index < periods.length - 1 | |||
? moment(periods[index + 1]) | |||
: createdBefore ? moment(createdBefore) : undefined; | |||
const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days'); | |||
let tooltip = formatMeasure(stats[startDate], 'SHORT_INT') + | |||
'<br>' + | |||
startMoment.format('LL'); | |||
if (endMoment) { | |||
const isSameDay = endMoment.diff(startMoment, 'days') <= 1; | |||
if (!isSameDay) { | |||
tooltip += ' – ' + endMoment.format('LL'); | |||
} | |||
} | |||
return { | |||
createdAfter: startMoment, | |||
createdBefore: nextStartMoment, | |||
startMoment, | |||
tooltip, | |||
x: index, | |||
y: stats[startDate] | |||
}; | |||
}); | |||
const barsWidth = Math.floor(240 / data.length); | |||
const width = barsWidth * data.length - 1 + 20; | |||
const maxValue = max(data.map(d => d.y)); | |||
const format = this.props.facetMode === 'count' ? 'SHORT_INT' : 'SHORT_WORK_DUR'; | |||
const xValues = data.map(d => d.y === maxValue ? formatMeasure(maxValue, format) : ''); | |||
return ( | |||
<BarChart | |||
barsWidth={barsWidth - 1} | |||
data={data} | |||
height={75} | |||
onBarClick={this.handleBarClick} | |||
padding={[25, 10, 5, 10]} | |||
width={width} | |||
xValues={xValues} | |||
/> | |||
); | |||
} | |||
renderExactDate() { | |||
const m = moment(this.props.createdAt); | |||
return ( | |||
<div className="search-navigator-facet-container"> | |||
{m.format('LLL')} | |||
<br /> | |||
<span className="note">({m.fromNow()})</span> | |||
</div> | |||
); | |||
} | |||
renderPeriodSelectors() { | |||
const { createdAfter, createdBefore } = this.props; | |||
return ( | |||
<div className="search-navigator-date-facet-selection"> | |||
<DateInput | |||
className="search-navigator-date-facet-selection-dropdown-left" | |||
onChange={this.handlePeriodChange('createdAfter')} | |||
placeholder={translate('from')} | |||
value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined} | |||
/> | |||
<DateInput | |||
className="search-navigator-date-facet-selection-dropdown-right" | |||
onChange={this.handlePeriodChange('createdBefore')} | |||
placeholder={translate('to')} | |||
value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined} | |||
/> | |||
</div> | |||
); | |||
} | |||
renderPrefefinedPeriods() { | |||
const { component, createdInLast, sinceLeakPeriod } = this.props; | |||
return ( | |||
<div className="spacer-top"> | |||
<span className="spacer-right">{translate('issues.facet.createdAt.or')}</span> | |||
<a className="spacer-right" href="#" onClick={this.handlePeriodClick()}> | |||
{translate('issues.facet.createdAt.all')} | |||
</a> | |||
{component == null && | |||
<a | |||
className={classNames('spacer-right', { 'active-link': createdInLast === '1w' })} | |||
href="#" | |||
onClick={this.handlePeriodClick('1w')}> | |||
{translate('issues.facet.createdAt.last_week')} | |||
</a>} | |||
{component == null && | |||
<a | |||
className={classNames('spacer-right', { 'active-link': createdInLast === '1m' })} | |||
href="#" | |||
onClick={this.handlePeriodClick('1m')}> | |||
{translate('issues.facet.createdAt.last_month')} | |||
</a>} | |||
{component == null && | |||
<a | |||
className={classNames('spacer-right', { 'active-link': createdInLast === '1y' })} | |||
href="#" | |||
onClick={this.handlePeriodClick('1y')}> | |||
{translate('issues.facet.createdAt.last_year')} | |||
</a>} | |||
{component != null && | |||
<a | |||
className={classNames('spacer-right', { 'active-link': sinceLeakPeriod })} | |||
href="#" | |||
onClick={this.handleLeakPeriodClick()}> | |||
{translate('issues.leak_period')} | |||
</a>} | |||
</div> | |||
); | |||
} | |||
renderInner() { | |||
const { createdAt } = this.props; | |||
return createdAt | |||
? this.renderExactDate() | |||
: <div> | |||
{this.renderBarChart()} | |||
{this.renderPeriodSelectors()} | |||
{this.renderPrefefinedPeriods()} | |||
</div>; | |||
} | |||
render() { | |||
const hasValue = this.props.createdAfter.length > 0 || | |||
this.props.createdAt.length > 0 || | |||
this.props.createdBefore.length > 0 || | |||
this.props.createdInLast.length > 0 || | |||
this.props.sinceLeakPeriod; | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={hasValue} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && this.renderInner()} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,120 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import type { ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: { [string]: Array<string> }) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
referencedComponents: { [string]: ReferencedComponent }, | |||
directories: Array<string> | |||
|}; | |||
export default class DirectoryFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'directories'; | |||
handleItemClick = (itemValue: string) => { | |||
const { directories } = this.props; | |||
const newValue = sortBy( | |||
directories.includes(itemValue) | |||
? without(directories, itemValue) | |||
: [...directories, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getStat(directory: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[directory] : null; | |||
} | |||
renderName(directory: string): React.Element<*> | string { | |||
// `referencedComponents` are indexed by uuid | |||
// so we have to browse them all to find a matching one | |||
const { referencedComponents } = this.props; | |||
const uuid = Object.keys(referencedComponents).find( | |||
uuid => referencedComponents[uuid].key === directory | |||
); | |||
const name = uuid ? referencedComponents[uuid].name : directory; | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="DIR" /> | |||
{name} | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const directories = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.directories.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{directories.map(directory => ( | |||
<FacetItem | |||
active={this.props.directories.includes(directory)} | |||
facetMode={this.props.facetMode} | |||
key={directory} | |||
name={this.renderName(directory)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(directory)} | |||
value={directory} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,67 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: {}) => void | |||
|}; | |||
export default class FacetMode extends React.PureComponent { | |||
props: Props; | |||
property = 'facetMode'; | |||
handleItemClick = (itemValue: string) => { | |||
this.props.onChange({ [this.property]: itemValue }); | |||
}; | |||
render() { | |||
const { facetMode } = this.props; | |||
const modes = ['count', 'effort']; | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader name={translate('issues.facet.mode')} /> | |||
<FacetItemsList> | |||
{modes.map(mode => ( | |||
<FacetItem | |||
active={facetMode === mode} | |||
facetMode={this.props.facetMode} | |||
halfWidth={true} | |||
key={mode} | |||
name={translate('issues.facet.mode', mode)} | |||
onClick={this.handleItemClick} | |||
stat={null} | |||
value={mode} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,116 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import type { ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { collapsePath } from '../../../helpers/path'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: { [string]: Array<string> }) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
referencedComponents: { [string]: ReferencedComponent }, | |||
files: Array<string> | |||
|}; | |||
export default class FileFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'files'; | |||
handleItemClick = (itemValue: string) => { | |||
const { files } = this.props; | |||
const newValue = sortBy( | |||
files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getStat(file: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[file] : null; | |||
} | |||
renderName(file: string): React.Element<*> | string { | |||
const { referencedComponents } = this.props; | |||
const name = referencedComponents[file] | |||
? collapsePath(referencedComponents[file].path, 15) | |||
: file; | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="FIL" /> | |||
{name} | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const files = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.files.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{files.map(file => ( | |||
<FacetItem | |||
active={this.props.files.includes(file)} | |||
facetMode={this.props.facetMode} | |||
key={file} | |||
name={this.renderName(file)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(file)} | |||
value={file} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,114 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, uniq, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import LanguageFacetFooter from './LanguageFacetFooter'; | |||
import type { ReferencedLanguage } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: { [string]: Array<string> }) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
referencedLanguages: { [string]: ReferencedLanguage }, | |||
languages: Array<string> | |||
|}; | |||
export default class LanguageFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'languages'; | |||
handleItemClick = (itemValue: string) => { | |||
const { languages } = this.props; | |||
const newValue = sortBy( | |||
languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getLanguageName(language: string): string { | |||
const { referencedLanguages } = this.props; | |||
return referencedLanguages[language] ? referencedLanguages[language].name : language; | |||
} | |||
getStat(language: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[language] : null; | |||
} | |||
handleSelect = (language: string) => { | |||
const { languages } = this.props; | |||
this.props.onChange({ [this.property]: uniq([...languages, language]) }); | |||
}; | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const languages = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.languages.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{languages.map(language => ( | |||
<FacetItem | |||
active={this.props.languages.includes(language)} | |||
facetMode={this.props.facetMode} | |||
key={language} | |||
name={this.getLanguageName(language)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(language)} | |||
value={language} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />} | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { connect } from 'react-redux'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getLanguages } from '../../../store/rootReducer'; | |||
type Option = { label: string, value: string }; | |||
type Props = {| | |||
languages: Array<{ key: string, name: string }>, | |||
onSelect: (value: string) => void | |||
|}; | |||
class LanguageFacetFooter extends React.PureComponent { | |||
props: Props; | |||
handleChange = (option: Option) => { | |||
this.props.onSelect(option.value); | |||
}; | |||
render() { | |||
const options = this.props.languages.map(language => ({ | |||
label: language.name, | |||
value: language.key | |||
})); | |||
return ( | |||
<div className="search-navigator-facet-footer"> | |||
<Select | |||
autofocus={true} | |||
className="input-super-large" | |||
clearable={false} | |||
noResultsText={translate('select2.noMatches')} | |||
onChange={this.handleChange} | |||
options={options} | |||
placeholder={translate('search_verb')} | |||
searchable={true} | |||
/> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
languages: Object.values(getLanguages(state)) | |||
}); | |||
export default connect(mapStateToProps)(LanguageFacetFooter); |
@@ -0,0 +1,113 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import type { ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
facetMode: string, | |||
onChange: (changes: { [string]: Array<string> }) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
stats?: { [string]: number }, | |||
referencedComponents: { [string]: ReferencedComponent }, | |||
modules: Array<string> | |||
|}; | |||
export default class ModuleFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'modules'; | |||
handleItemClick = (itemValue: string) => { | |||
const { modules } = this.props; | |||
const newValue = sortBy( | |||
modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getStat(module: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[module] : null; | |||
} | |||
renderName(module: string): React.Element<*> | string { | |||
const { referencedComponents } = this.props; | |||
const name = referencedComponents[module] ? referencedComponents[module].name : module; | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
{name} | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const modules = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.modules.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{modules.map(module => ( | |||
<FacetItem | |||
active={this.props.modules.includes(module)} | |||
facetMode={this.props.facetMode} | |||
key={module} | |||
name={this.renderName(module)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(module)} | |||
value={module} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
</FacetBox> | |||
); | |||
} | |||
} |