@@ -17,7 +17,6 @@ | |||
"d3-selection": "1.0.5", | |||
"d3-shape": "1.0.6", | |||
"escape-html": "1.0.3", | |||
"glamor": "2.20.24", | |||
"handlebars": "2.0.0", | |||
"history": "2.0.0", | |||
"jquery": "2.2.0", |
@@ -62,7 +62,7 @@ export default class GlobalNavMenu extends React.PureComponent { | |||
renderIssuesLink() { | |||
const query = this.props.currentUser.isLoggedIn | |||
? { myIssues: 'true', resolved: 'false' } | |||
? { createdInLast: '1w', myIssues: 'true', resolved: 'false' } | |||
: { resolved: 'false' }; | |||
const active = this.props.location.pathname === 'issues'; | |||
return ( |
@@ -26,8 +26,7 @@ | |||
<h3 class="shortcuts-section-title">{{t 'shortcuts.section.rules'}}</h3> | |||
<ul class="shortcuts-list"> | |||
<li><span class="shortcut-button">↑</span> <span | |||
class="shortcut-button">↓</span> {{t 'shortcuts.section.rules.navigate_between_rules'}}</li> | |||
<li><span class="shortcut-button">↑</span> <span class="shortcut-button">↓</span> {{t 'shortcuts.section.rules.navigate_between_rules'}}</li> | |||
<li><span class="shortcut-button">→</span> {{t 'shortcuts.section.rules.open_details'}}</li> | |||
<li><span class="shortcut-button">←</span> {{t 'shortcuts.section.rules.return_to_list'}}</li> | |||
<li><span class="shortcut-button">a</span> {{t 'shortcuts.section.rules.activate'}}</li> | |||
@@ -38,12 +37,22 @@ | |||
<div class="column-half"> | |||
<h3 class="shortcuts-section-title">{{t 'shortcuts.section.issues'}}</h3> | |||
<ul class="shortcuts-list"> | |||
<li><span class="shortcut-button">↑</span> <span | |||
class="shortcut-button">↓</span> {{t 'shortcuts.section.issues.navigate_between_issues'}} | |||
<li><span class="shortcut-button">↑</span> <span class="shortcut-button">↓</span> {{t 'shortcuts.section.issues.navigate_between_issues'}} | |||
</li> | |||
<li><span class="shortcut-button">→</span> {{t 'shortcuts.section.issues.open_details'}}</li> | |||
<li><span class="shortcut-button">←</span> {{t 'shortcuts.section.issues.return_to_list'}}</li> | |||
<li><span class="shortcut-button">⎵</span> {{t 'shortcuts.section.issue.select'}}</li> | |||
<li> | |||
<span class="shortcut-button">alt</span> | |||
<span class=>+</span> | |||
<span class="shortcut-button">↑</span> | |||
<span class="shortcut-button">↓</span> {{t 'issues.to_navigate_issue_locations'}} | |||
</li> | |||
<li> | |||
<span class="shortcut-button">alt</span> | |||
<span class=>+</span> | |||
<span class="shortcut-button">←</span> | |||
<span class="shortcut-button">→</span> {{t 'issues.to_switch_flows'}} | |||
</li> | |||
<li><span class="shortcut-button">f</span> {{t 'shortcuts.section.issue.do_transition'}}</li> | |||
<li><span class="shortcut-button">a</span> {{t 'shortcuts.section.issue.assign'}}</li> | |||
<li><span class="shortcut-button">m</span> {{t 'shortcuts.section.issue.assign_to_me'}}</li> | |||
@@ -58,4 +67,4 @@ | |||
<div class="modal-foot"> | |||
<a class="js-modal-close" href="#">{{t 'close'}}</a> | |||
</div> | |||
</div> |
@@ -20,7 +20,7 @@ | |||
import $ from 'jquery'; | |||
import Marionette from 'backbone.marionette'; | |||
import Template from '../templates/rule/coding-rules-rule-issues.hbs'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
import { getComponentIssuesUrlAsString } from '../../../helpers/urls'; | |||
export default Marionette.ItemView.extend({ | |||
template: Template, | |||
@@ -55,7 +55,7 @@ export default Marionette.ItemView.extend({ | |||
...project, | |||
name: projectBase != null ? projectBase.longName : '', | |||
issuesUrl: projectBase != null && | |||
getComponentIssuesUrl(projectBase.key, { | |||
getComponentIssuesUrlAsString(projectBase.key, { | |||
resolved: 'false', | |||
rules: this.model.id | |||
}) |
@@ -20,12 +20,16 @@ | |||
// @flow | |||
import type { State } from './components/App'; | |||
export const enableLocationsNavigator = (state: State) => ({ | |||
locationsNavigator: true, | |||
selectedFlowIndex: state.selectedFlowIndex || | |||
(state.openIssue && state.openIssue.flows.length > 0 ? 0 : null), | |||
selectedLocationIndex: state.selectedLocationIndex || 0 | |||
}); | |||
export const enableLocationsNavigator = (state: State) => { | |||
const { openIssue } = state; | |||
if (openIssue && (openIssue.secondaryLocations.length > 0 || openIssue.flows.length > 0)) { | |||
return { | |||
locationsNavigator: true, | |||
selectedFlowIndex: state.selectedFlowIndex || (openIssue.flows.length > 0 ? 0 : null), | |||
selectedLocationIndex: state.selectedLocationIndex || 0 | |||
}; | |||
} | |||
}; | |||
export const disableLocationsNavigator = () => ({ | |||
locationsNavigator: false | |||
@@ -70,3 +74,17 @@ export const selectPreviousLocation = (state: State) => { | |||
export const selectFlow = (nextIndex: ?number) => () => { | |||
return { selectedFlowIndex: nextIndex, selectedLocationIndex: 0 }; | |||
}; | |||
export const selectNextFlow = (state: State) => { | |||
const { openIssue, selectedFlowIndex } = state; | |||
if (openIssue && selectedFlowIndex != null && openIssue.flows.length > selectedFlowIndex + 1) { | |||
return { selectedFlowIndex: selectedFlowIndex + 1, selectedLocationIndex: 0 }; | |||
} | |||
}; | |||
export const selectPreviousFlow = (state: State) => { | |||
const { openIssue, selectedFlowIndex } = state; | |||
if (openIssue && selectedFlowIndex != null && selectedFlowIndex > 0) { | |||
return { selectedFlowIndex: selectedFlowIndex - 1, selectedLocationIndex: 0 }; | |||
} | |||
}; |
@@ -39,7 +39,8 @@ import { | |||
areQueriesEqual, | |||
getOpen, | |||
serializeQuery, | |||
parseFacets | |||
parseFacets, | |||
mapFacet | |||
} from '../utils'; | |||
import type { | |||
Query, | |||
@@ -53,11 +54,6 @@ import type { | |||
} from '../utils'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import EmptySearch from '../../../components/common/EmptySearch'; | |||
import Page from '../../../components/layout/Page'; | |||
import PageMain from '../../../components/layout/PageMain'; | |||
import PageMainInner from '../../../components/layout/PageMainInner'; | |||
import PageSide from '../../../components/layout/PageSide'; | |||
import PageFilters from '../../../components/layout/PageFilters'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
@@ -227,6 +223,14 @@ export default class App extends React.PureComponent { | |||
// alt + down | |||
event.preventDefault(); | |||
this.selectPreviousLocation(); | |||
} else if (event.keyCode === 37 && event.altKey) { | |||
// alt + left | |||
event.preventDefault(); | |||
this.selectPreviousFlow(); | |||
} else if (event.keyCode === 39 && event.altKey) { | |||
// alt + right | |||
event.preventDefault(); | |||
this.selectNextFlow(); | |||
} | |||
}; | |||
@@ -311,6 +315,7 @@ export default class App extends React.PureComponent { | |||
open: undefined | |||
} | |||
}); | |||
this.scrollToSelectedIssue(false); | |||
} | |||
}; | |||
@@ -321,43 +326,30 @@ export default class App extends React.PureComponent { | |||
} | |||
}; | |||
scrollToSelectedIssue = () => { | |||
scrollToSelectedIssue = (smooth: boolean = true) => { | |||
const { selected } = this.state; | |||
if (selected) { | |||
const element = document.querySelector(`[data-issue="${selected}"]`); | |||
if (element) { | |||
scrollToElement(element, 150, 100); | |||
scrollToElement(element, { topOffset: 150, bottomOffset: 100, smooth }); | |||
} | |||
} | |||
}; | |||
fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => { | |||
const { component } = this.props; | |||
const { myIssues, query } = this.state; | |||
const { myIssues, openFacets, query } = this.state; | |||
const facets = requestFacets | |||
? Object.keys(openFacets).filter(facet => openFacets[facet]).map(mapFacet).join(',') | |||
: undefined; | |||
const parameters = { | |||
componentKeys: component && component.key, | |||
...serializeQuery(query), | |||
s: 'FILE_LINE', | |||
ps: 25, | |||
facets: requestFacets | |||
? [ | |||
'assignees', | |||
'authors', | |||
'createdAt', | |||
'directories', | |||
'fileUuids', | |||
'languages', | |||
'moduleUuids', | |||
'projectUuids', | |||
'resolutions', | |||
'rules', | |||
'severities', | |||
'statuses', | |||
'tags', | |||
'types' | |||
].join() | |||
: undefined, | |||
ps: 100, | |||
facets, | |||
...additional | |||
}; | |||
@@ -464,6 +456,32 @@ export default class App extends React.PureComponent { | |||
}); | |||
}; | |||
fetchFacet = (facet: string) => { | |||
return this.fetchIssues({ ps: 1, facets: mapFacet(facet) }).then(({ facets, ...other }) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
facets: { ...state.facets, ...parseFacets(facets) }, | |||
referencedComponents: { | |||
...state.referencedComponents, | |||
...keyBy(other.components, 'uuid') | |||
}, | |||
referencedLanguages: { | |||
...state.referencedLanguages, | |||
...keyBy(other.languages, 'key') | |||
}, | |||
referencedRules: { | |||
...state.referencedRules, | |||
...keyBy(other.rules, 'key') | |||
}, | |||
referencedUsers: { | |||
...state.referencedUsers, | |||
...keyBy(other.users, 'login') | |||
} | |||
})); | |||
} | |||
}); | |||
}; | |||
isFiltered = () => { | |||
const serialized = serializeQuery(this.state.query); | |||
return !areQueriesEqual(serialized, DEFAULT_QUERY); | |||
@@ -510,6 +528,9 @@ export default class App extends React.PureComponent { | |||
this.setState(state => ({ | |||
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] } | |||
})); | |||
if (!this.state.facets[property]) { | |||
this.fetchFacet(property); | |||
} | |||
}; | |||
handleReset = () => { | |||
@@ -564,6 +585,10 @@ export default class App extends React.PureComponent { | |||
this.closeBulkChange(); | |||
}; | |||
handleReload = () => { | |||
this.fetchFirstIssues(); | |||
}; | |||
handleReloadAndOpenFirst = () => { | |||
this.fetchFirstIssues().then(issues => { | |||
if (issues.length > 0) { | |||
@@ -576,6 +601,8 @@ export default class App extends React.PureComponent { | |||
selectNextLocation = () => this.setState(actions.selectNextLocation); | |||
selectPreviousLocation = () => this.setState(actions.selectPreviousLocation); | |||
selectFlow = (index: ?number) => this.setState(actions.selectFlow(index)); | |||
selectNextFlow = () => this.setState(actions.selectNextFlow); | |||
selectPreviousFlow = () => this.setState(actions.selectPreviousFlow); | |||
renderBulkChange(openIssue: ?Issue) { | |||
const { component, currentUser } = this.props; | |||
@@ -627,7 +654,7 @@ export default class App extends React.PureComponent { | |||
const { query } = this.state; | |||
return ( | |||
<PageFilters> | |||
<div className="layout-page-filters"> | |||
{currentUser.isLoggedIn && | |||
<MyIssuesFilter | |||
myIssues={this.state.myIssues} | |||
@@ -647,7 +674,7 @@ export default class App extends React.PureComponent { | |||
referencedRules={this.state.referencedRules} | |||
referencedUsers={this.state.referencedUsers} | |||
/> | |||
</PageFilters> | |||
</div> | |||
); | |||
} | |||
@@ -655,7 +682,7 @@ export default class App extends React.PureComponent { | |||
const { issues, paging } = this.state; | |||
return ( | |||
<PageFilters> | |||
<div className="layout-page-filters"> | |||
<ConciseIssuesListHeader | |||
loading={this.state.loading} | |||
onBackClick={this.closeIssue} | |||
@@ -675,7 +702,7 @@ export default class App extends React.PureComponent { | |||
{paging != null && | |||
paging.total > 0 && | |||
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />} | |||
</PageFilters> | |||
</div> | |||
); | |||
} | |||
@@ -683,24 +710,28 @@ export default class App extends React.PureComponent { | |||
const top = this.props.component ? 95 : 30; | |||
return ( | |||
<PageSide top={top}> | |||
{openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()} | |||
</PageSide> | |||
<div className="layout-page-side-outer"> | |||
<div className="layout-page-side" style={{ top }}> | |||
<div className="layout-page-side-inner"> | |||
{openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()} | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
renderList(openIssue: ?Issue) { | |||
renderList() { | |||
const { component, currentUser } = this.props; | |||
const { issues, paging } = this.state; | |||
const { issues, openIssue, paging } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null; | |||
if (paging == null) { | |||
if (paging == null || openIssue != null) { | |||
return null; | |||
} | |||
return ( | |||
<div className={openIssue != null ? 'hidden' : undefined}> | |||
<div> | |||
{paging.total > 0 && | |||
<IssuesList | |||
checked={this.state.checked} | |||
@@ -722,12 +753,22 @@ export default class App extends React.PureComponent { | |||
} | |||
renderShortcutsForLocations() { | |||
const { openIssue } = this.state; | |||
if (openIssue == null || (!openIssue.secondaryLocations.length && !openIssue.flows.length)) { | |||
return null; | |||
} | |||
const hasSeveralFlows = openIssue.flows.length > 1; | |||
return ( | |||
<div className="pull-right note"> | |||
<span className="shortcut-button little-spacer-right">alt</span> | |||
<span className="little-spacer-right">{'+'}</span> | |||
<span className="shortcut-button little-spacer-right">↑</span> | |||
<span className="shortcut-button little-spacer-right">↓</span> | |||
{hasSeveralFlows && | |||
<span> | |||
<span className="shortcut-button little-spacer-right">←</span> | |||
<span className="shortcut-button little-spacer-right">→</span> | |||
</span>} | |||
{translate('issues.to_navigate_issue_locations')} | |||
</div> | |||
); | |||
@@ -740,50 +781,50 @@ export default class App extends React.PureComponent { | |||
const selectedIndex = this.getSelectedIndex(); | |||
return ( | |||
<Page className="issues" id="issues-page"> | |||
<div className="layout-page issues" id="issues-page"> | |||
<Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" /> | |||
{this.renderSide(openIssue)} | |||
<PageMain> | |||
<div className="layout-page-main"> | |||
<div className="issues-header-panel issues-main-header"> | |||
<div className="issues-header-panel-inner issues-main-header-inner"> | |||
<PageMainInner> | |||
<div className="layout-page-main-inner"> | |||
{this.renderBulkChange(openIssue)} | |||
{openIssue != null | |||
? <div className="pull-left"> | |||
? <div className="pull-left width-60"> | |||
<ComponentBreadcrumbs component={component} issue={openIssue} /> | |||
</div> | |||
: <PageActions | |||
loading={this.state.loading} | |||
onReload={this.handleReload} | |||
paging={paging} | |||
selectedIndex={selectedIndex} | |||
/>} | |||
{openIssue != null && this.renderShortcutsForLocations()} | |||
</PageMainInner> | |||
{this.renderShortcutsForLocations()} | |||
</div> | |||
</div> | |||
</div> | |||
<PageMainInner> | |||
<div className="layout-page-main-inner"> | |||
<div> | |||
{openIssue != null && | |||
<IssuesSourceViewer | |||
openIssue={openIssue} | |||
loadIssues={this.fetchIssuesForComponent} | |||
onIssueChange={this.handleIssueChange} | |||
onIssueSelect={this.openIssue} | |||
onLocationSelect={this.selectLocation} | |||
selectedFlowIndex={this.state.selectedFlowIndex} | |||
selectedLocationIndex={ | |||
this.state.locationsNavigator ? this.state.selectedLocationIndex : null | |||
} | |||
/>} | |||
{this.renderList(openIssue)} | |||
{openIssue | |||
? <IssuesSourceViewer | |||
openIssue={openIssue} | |||
loadIssues={this.fetchIssuesForComponent} | |||
onIssueChange={this.handleIssueChange} | |||
onIssueSelect={this.openIssue} | |||
onLocationSelect={this.selectLocation} | |||
selectedFlowIndex={this.state.selectedFlowIndex} | |||
selectedLocationIndex={ | |||
this.state.locationsNavigator ? this.state.selectedLocationIndex : null | |||
} | |||
/> | |||
: this.renderList()} | |||
</div> | |||
</PageMainInner> | |||
</PageMain> | |||
</Page> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -21,7 +21,6 @@ | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import Select from 'react-select'; | |||
import { css } from 'glamor'; | |||
import { pickBy, sortBy } from 'lodash'; | |||
import SearchSelect from './SearchSelect'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
@@ -228,11 +227,7 @@ export default class BulkChangeModal extends React.PureComponent { | |||
); | |||
renderCheckbox = (field: string) => ( | |||
<Checkbox | |||
className={css({ paddingTop: 6, paddingRight: 8 })} | |||
checked={this.state[field] != null} | |||
onCheck={this.handleFieldCheck(field)} | |||
/> | |||
<Checkbox checked={this.state[field] != null} onCheck={this.handleFieldCheck(field)} /> | |||
); | |||
renderAffected = (affected: number) => ( |
@@ -42,7 +42,7 @@ export default class ComponentBreadcrumbs extends React.PureComponent { | |||
const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier); | |||
return ( | |||
<div className="component-name"> | |||
<div className="component-name text-ellipsis"> | |||
{displayOrganization && | |||
<Organization linkClassName="link-no-underline" organizationKey={issue.organization} />} | |||
@@ -19,7 +19,6 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
@@ -27,8 +26,6 @@ type Props = { | |||
onReset: () => void | |||
}; | |||
const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' }); | |||
export default class FiltersHeader extends React.PureComponent { | |||
props: Props; | |||
@@ -40,9 +37,9 @@ export default class FiltersHeader extends React.PureComponent { | |||
render() { | |||
return ( | |||
<div className={styles}> | |||
<div className="issues-filters-header"> | |||
{this.props.displayReset && | |||
<div className={css({ float: 'right' })}> | |||
<div className="pull-right"> | |||
<button className="button-red" onClick={this.handleResetClick}> | |||
{translate('clear_all_filters')} | |||
</button> |
@@ -46,16 +46,20 @@ export default class IssuesSourceViewer extends React.PureComponent { | |||
} | |||
} | |||
scrollToIssue = () => { | |||
scrollToIssue = (smooth: boolean = true) => { | |||
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`); | |||
if (element) { | |||
this.handleScroll(element); | |||
this.handleScroll(element, smooth); | |||
} | |||
}; | |||
handleScroll = (element: HTMLElement) => { | |||
handleScroll = (element: HTMLElement, smooth: boolean = true) => { | |||
const offset = window.innerHeight / 2; | |||
scrollToElement(element, offset - 100, offset); | |||
scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth }); | |||
}; | |||
handleLoaded = () => { | |||
this.scrollToIssue(false); | |||
}; | |||
render() { | |||
@@ -80,7 +84,7 @@ export default class IssuesSourceViewer extends React.PureComponent { | |||
highlightedLocations={locations} | |||
highlightedLocationMessage={locationMessage} | |||
loadIssues={this.props.loadIssues} | |||
onLoaded={this.scrollToIssue} | |||
onLoaded={this.handleLoaded} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssueSelect={this.props.onIssueSelect} |
@@ -19,7 +19,6 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
@@ -40,7 +39,7 @@ export default class MyIssuesFilter extends React.PureComponent { | |||
const { myIssues } = this.props; | |||
return ( | |||
<div className={css({ marginBottom: 24, textAlign: 'center' })}> | |||
<div className="issues-my-issues-filter"> | |||
<div className="button-group"> | |||
<button | |||
className={myIssues ? 'button-active' : undefined} |
@@ -19,13 +19,14 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import IssuesCounter from './IssuesCounter'; | |||
import ReloadButton from './ReloadButton'; | |||
import type { Paging } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
loading: boolean, | |||
onReload: () => void, | |||
paging: ?Paging, | |||
selectedIndex: ?number | |||
|}; | |||
@@ -55,11 +56,13 @@ export default class PageActions extends React.PureComponent { | |||
const { paging, selectedIndex } = this.props; | |||
return ( | |||
<div className={css({ float: 'right' })}> | |||
<div className="pull-right"> | |||
{this.renderShortcuts()} | |||
<div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}> | |||
{this.props.loading && <i className="spinner spacer-right" />} | |||
<div className="issues-page-actions"> | |||
{this.props.loading | |||
? <i className="issues-main-header-spinner spinner" /> | |||
: <ReloadButton className="spacer-right" onClick={this.props.onReload} />} | |||
{paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />} | |||
</div> | |||
</div> |
@@ -20,6 +20,8 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
className?: string, | |||
@@ -41,11 +43,13 @@ export default function ReloadButton(props: Props) { | |||
}; | |||
return ( | |||
<a | |||
className={classNames('concise-issues-list-header-button', props.className)} | |||
href="#" | |||
onClick={handleClick}> | |||
{icon} | |||
</a> | |||
<Tooltip overlay={translate('reload')}> | |||
<a | |||
className={classNames('concise-issues-list-header-button', props.className)} | |||
href="#" | |||
onClick={handleClick}> | |||
{icon} | |||
</a> | |||
</Tooltip> | |||
); | |||
} |
@@ -20,6 +20,8 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
className?: string, | |||
@@ -41,11 +43,13 @@ export default function BackButton(props: Props) { | |||
}; | |||
return ( | |||
<a | |||
className={classNames('concise-issues-list-header-button', props.className)} | |||
href="#" | |||
onClick={handleClick}> | |||
{icon} | |||
</a> | |||
<Tooltip overlay={translate('issues.return_to_list')}> | |||
<a | |||
className={classNames('concise-issues-list-header-button', props.className)} | |||
href="#" | |||
onClick={handleClick}> | |||
{icon} | |||
</a> | |||
</Tooltip> | |||
); | |||
} |
@@ -31,30 +31,47 @@ type Props = {| | |||
onClick: string => void, | |||
onFlowSelect: number => void, | |||
onLocationSelect: number => void, | |||
scroll: HTMLElement => void, | |||
scroll: (element: HTMLElement, bottomOffset: ?number) => void, | |||
selected: boolean, | |||
selectedFlowIndex: ?number, | |||
selectedLocationIndex: ?number | |||
|}; | |||
export default class ConciseIssueBox extends React.PureComponent { | |||
node: HTMLElement; | |||
messageElement: HTMLElement; | |||
rootElement: HTMLElement; | |||
props: Props; | |||
componentDidMount() { | |||
// scroll to the message element and not to the root element, | |||
// because the root element can be huge and exceed the window height | |||
if (this.props.selected) { | |||
this.props.scroll(this.node); | |||
this.handleScroll(); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.selected && prevProps.selected !== this.props.selected) { | |||
this.props.scroll(this.node); | |||
this.handleScroll(); | |||
} | |||
} | |||
handleScroll = () => { | |||
const { selectedFlowIndex } = this.props; | |||
const { flows, secondaryLocations } = this.props.issue; | |||
const locations = selectedFlowIndex != null | |||
? flows[selectedFlowIndex] | |||
: flows.length > 0 ? flows[0] : secondaryLocations; | |||
if (locations == null || locations.length < 15) { | |||
// if there are no locations, or there are just few | |||
// then ensuse that the whole box is visible | |||
this.props.scroll(this.rootElement); | |||
} else { | |||
// otherwise scroll until the the message element is located on top | |||
this.props.scroll(this.messageElement, window.innerHeight - 200); | |||
} | |||
}; | |||
handleClick = (event: Event) => { | |||
event.preventDefault(); | |||
this.props.onClick(this.props.issue.key); | |||
@@ -70,8 +87,9 @@ export default class ConciseIssueBox extends React.PureComponent { | |||
return ( | |||
<div | |||
className={classNames('concise-issue-box', 'clearfix', { selected })} | |||
ref={node => (this.rootElement = node)} | |||
{...clickAttributes}> | |||
<div className="concise-issue-box-message" ref={node => (this.node = node)}> | |||
<div className="concise-issue-box-message" ref={node => (this.messageElement = node)}> | |||
{issue.message} | |||
</div> | |||
<div className="concise-issue-box-attributes"> |
@@ -36,10 +36,10 @@ type Props = {| | |||
export default class ConciseIssuesList extends React.PureComponent { | |||
props: Props; | |||
handleScroll = (element: HTMLElement) => { | |||
handleScroll = (element: HTMLElement, bottomOffset: number = 100) => { | |||
const scrollableElement = document.querySelector('.layout-page-side'); | |||
if (element && scrollableElement) { | |||
scrollToElement(element, 150, 100, scrollableElement); | |||
scrollToElement(element, { topOffset: 150, bottomOffset, parent: scrollableElement }); | |||
} | |||
}; | |||
@@ -20,7 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import BackButton from './BackButton'; | |||
import ReloadButton from './ReloadButton'; | |||
import ReloadButton from '../components/ReloadButton'; | |||
import IssuesCounter from '../components/IssuesCounter'; | |||
import type { Paging } from '../utils'; | |||
@@ -69,6 +69,10 @@ export default class AssigneeFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ assigned: true, assignees: [] }); | |||
}; | |||
handleSearch = (query: string) => searchAssignees(query, this.props.component); | |||
handleSelect = (assignee: string) => { | |||
@@ -117,7 +121,7 @@ export default class AssigneeFacet extends React.PureComponent { | |||
); | |||
}; | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -132,36 +136,50 @@ export default class AssigneeFacet extends React.PureComponent { | |||
key => -stats[key] | |||
); | |||
return ( | |||
<FacetItemsList> | |||
{assignees.map(assignee => ( | |||
<FacetItem | |||
active={this.isAssigneeActive(assignee)} | |||
facetMode={this.props.facetMode} | |||
key={assignee} | |||
name={this.getAssigneeName(assignee)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(assignee)} | |||
value={assignee} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderFooter() { | |||
if (!this.props.stats) { | |||
return null; | |||
} | |||
return ( | |||
<FacetFooter | |||
onSearch={this.handleSearch} | |||
onSelect={this.handleSelect} | |||
renderOption={this.renderOption} | |||
/> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={!this.props.assigned || this.props.assignees.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.assignees.length + (this.props.assigned ? 0 : 1)} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{assignees.map(assignee => ( | |||
<FacetItem | |||
active={this.isAssigneeActive(assignee)} | |||
facetMode={this.props.facetMode} | |||
key={assignee} | |||
name={this.getAssigneeName(assignee)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(assignee)} | |||
value={assignee} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && | |||
<FacetFooter | |||
onSearch={this.handleSearch} | |||
onSelect={this.handleSelect} | |||
renderOption={this.renderOption} | |||
/>} | |||
{this.props.open && this.renderList()} | |||
{this.props.open && this.renderFooter()} | |||
</FacetBox> | |||
); | |||
} |
@@ -56,12 +56,16 @@ export default class AuthorFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(author: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[author] : null; | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -70,29 +74,35 @@ export default class AuthorFacet extends React.PureComponent { | |||
const authors = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{authors.map(author => ( | |||
<FacetItem | |||
active={this.props.authors.includes(author)} | |||
facetMode={this.props.facetMode} | |||
key={author} | |||
name={author} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(author)} | |||
value={author} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.authors.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.authors.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{authors.map(author => ( | |||
<FacetItem | |||
active={this.props.authors.includes(author)} | |||
facetMode={this.props.facetMode} | |||
key={author} | |||
name={author} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(author)} | |||
value={author} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && this.renderList()} | |||
</FacetBox> | |||
); | |||
} |
@@ -59,6 +59,10 @@ export default class CreationDateFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.resetTo({}); | |||
}; | |||
resetTo = (changes: {}) => { | |||
this.props.onChange({ | |||
createdAfter: undefined, | |||
@@ -252,19 +256,14 @@ export default class CreationDateFacet extends React.PureComponent { | |||
this.props.createdInLast.length > 0 || | |||
this.props.sinceLeakPeriod; | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={hasValue} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={hasValue ? 1 : 0} | |||
/> | |||
{this.props.open && this.renderInner()} |
@@ -61,6 +61,10 @@ export default class DirectoryFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(directory: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[directory] : null; | |||
@@ -82,7 +86,7 @@ export default class DirectoryFacet extends React.PureComponent { | |||
); | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -91,29 +95,35 @@ export default class DirectoryFacet extends React.PureComponent { | |||
const directories = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{directories.map(directory => ( | |||
<FacetItem | |||
active={this.props.directories.includes(directory)} | |||
facetMode={this.props.facetMode} | |||
key={directory} | |||
name={this.renderName(directory)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(directory)} | |||
value={directory} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.directories.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.directories.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{directories.map(directory => ( | |||
<FacetItem | |||
active={this.props.directories.includes(directory)} | |||
facetMode={this.props.facetMode} | |||
key={directory} | |||
name={this.renderName(directory)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(directory)} | |||
value={directory} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && this.renderList()} | |||
</FacetBox> | |||
); | |||
} |
@@ -60,6 +60,10 @@ export default class FileFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(file: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[file] : null; | |||
@@ -78,7 +82,7 @@ export default class FileFacet extends React.PureComponent { | |||
); | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -87,29 +91,35 @@ export default class FileFacet extends React.PureComponent { | |||
const files = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{files.map(file => ( | |||
<FacetItem | |||
active={this.props.files.includes(file)} | |||
facetMode={this.props.facetMode} | |||
key={file} | |||
name={this.renderName(file)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(file)} | |||
value={file} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.files.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.files.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{files.map(file => ( | |||
<FacetItem | |||
active={this.props.files.includes(file)} | |||
facetMode={this.props.facetMode} | |||
key={file} | |||
name={this.renderName(file)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(file)} | |||
value={file} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && this.renderList()} | |||
</FacetBox> | |||
); | |||
} |
@@ -59,6 +59,10 @@ export default class LanguageFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getLanguageName(language: string): string { | |||
const { referencedLanguages } = this.props; | |||
return referencedLanguages[language] ? referencedLanguages[language].name : language; | |||
@@ -74,7 +78,7 @@ export default class LanguageFacet extends React.PureComponent { | |||
this.props.onChange({ [this.property]: uniq([...languages, language]) }); | |||
}; | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -83,31 +87,44 @@ export default class LanguageFacet extends React.PureComponent { | |||
const languages = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{languages.map(language => ( | |||
<FacetItem | |||
active={this.props.languages.includes(language)} | |||
facetMode={this.props.facetMode} | |||
key={language} | |||
name={this.getLanguageName(language)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(language)} | |||
value={language} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderFooter() { | |||
if (!this.props.stats) { | |||
return null; | |||
} | |||
return <LanguageFacetFooter onSelect={this.handleSelect} />; | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.languages.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.languages.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{languages.map(language => ( | |||
<FacetItem | |||
active={this.props.languages.includes(language)} | |||
facetMode={this.props.facetMode} | |||
key={language} | |||
name={this.getLanguageName(language)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(language)} | |||
value={language} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />} | |||
{this.props.open && this.renderList()} | |||
{this.props.open && this.renderFooter()} | |||
</FacetBox> | |||
); | |||
} |
@@ -59,6 +59,10 @@ export default class ModuleFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(module: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[module] : null; | |||
@@ -75,7 +79,7 @@ export default class ModuleFacet extends React.PureComponent { | |||
); | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -84,29 +88,35 @@ export default class ModuleFacet extends React.PureComponent { | |||
const modules = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{modules.map(module => ( | |||
<FacetItem | |||
active={this.props.modules.includes(module)} | |||
facetMode={this.props.facetMode} | |||
key={module} | |||
name={this.renderName(module)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(module)} | |||
value={module} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.modules.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.modules.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{modules.map(module => ( | |||
<FacetItem | |||
active={this.props.modules.includes(module)} | |||
facetMode={this.props.facetMode} | |||
key={module} | |||
name={this.renderName(module)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(module)} | |||
value={module} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && this.renderList()} | |||
</FacetBox> | |||
); | |||
} |
@@ -63,6 +63,10 @@ export default class ProjectFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
handleSearch = (query: string) => { | |||
const { component } = this.props; | |||
@@ -116,7 +120,7 @@ export default class ProjectFacet extends React.PureComponent { | |||
); | |||
}; | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -125,37 +129,51 @@ export default class ProjectFacet extends React.PureComponent { | |||
const projects = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{projects.map(project => ( | |||
<FacetItem | |||
active={this.props.projects.includes(project)} | |||
facetMode={this.props.facetMode} | |||
key={project} | |||
name={this.renderName(project)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(project)} | |||
value={project} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderFooter() { | |||
if (!this.props.stats) { | |||
return null; | |||
} | |||
return ( | |||
<FacetFooter | |||
minimumQueryLength={3} | |||
onSearch={this.handleSearch} | |||
onSelect={this.handleSelect} | |||
renderOption={this.renderOption} | |||
/> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.projects.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.projects.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{projects.map(project => ( | |||
<FacetItem | |||
active={this.props.projects.includes(project)} | |||
facetMode={this.props.facetMode} | |||
key={project} | |||
name={this.renderName(project)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(project)} | |||
value={project} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && | |||
<FacetFooter | |||
minimumQueryLength={3} | |||
onSearch={this.handleSearch} | |||
onSelect={this.handleSelect} | |||
renderOption={this.renderOption} | |||
/>} | |||
{this.props.open && this.renderList()} | |||
{this.props.open && this.renderFooter()} | |||
</FacetBox> | |||
); | |||
} |
@@ -65,6 +65,10 @@ export default class ResolutionFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ resolved: false, resolutions: [] }); | |||
}; | |||
isFacetItemActive(resolution: string) { | |||
return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution); | |||
} | |||
@@ -103,10 +107,11 @@ export default class ResolutionFacet extends React.PureComponent { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={!this.props.resolved || this.props.resolutions.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.resolutions.length} | |||
/> | |||
{this.props.open && |
@@ -60,6 +60,10 @@ export default class RuleFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
handleSearch = (query: string) => { | |||
const { languages } = this.props; | |||
return searchRules({ | |||
@@ -86,7 +90,7 @@ export default class RuleFacet extends React.PureComponent { | |||
return stats ? stats[rule] : null; | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -95,32 +99,44 @@ export default class RuleFacet extends React.PureComponent { | |||
const rules = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{rules.map(rule => ( | |||
<FacetItem | |||
active={this.props.rules.includes(rule)} | |||
facetMode={this.props.facetMode} | |||
key={rule} | |||
name={this.getRuleName(rule)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(rule)} | |||
value={rule} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderFooter() { | |||
if (!this.props.stats) { | |||
return null; | |||
} | |||
return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />; | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.rules.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.rules.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{rules.map(rule => ( | |||
<FacetItem | |||
active={this.props.rules.includes(rule)} | |||
facetMode={this.props.facetMode} | |||
key={rule} | |||
name={this.getRuleName(rule)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(rule)} | |||
value={rule} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && | |||
<FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />} | |||
{this.props.open && this.renderList()} | |||
{this.props.open && this.renderFooter()} | |||
</FacetBox> | |||
); | |||
} |
@@ -57,6 +57,10 @@ export default class SeverityFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(severity: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[severity] : null; | |||
@@ -87,10 +91,11 @@ export default class SeverityFacet extends React.PureComponent { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.severities.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.severities.length} | |||
/> | |||
{this.props.open && |
@@ -65,8 +65,9 @@ export default class Sidebar extends React.PureComponent { | |||
const displayProjectsFacet: boolean = | |||
component == null || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier); | |||
const displayModulesFacet = component == null || component.qualifier !== 'DIR'; | |||
const displayDirectoriesFacet = component == null || component.qualifier !== 'DIR'; | |||
const displayModulesFacet = component != null && component.qualifier !== 'DIR'; | |||
const displayDirectoriesFacet = component != null && component.qualifier !== 'DIR'; | |||
const displayFilesFacet = component != null; | |||
const displayAuthorFacet = component == null || component.qualifier !== 'DEV'; | |||
return ( | |||
@@ -167,15 +168,16 @@ export default class Sidebar extends React.PureComponent { | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.directories} | |||
/>} | |||
<FileFacet | |||
facetMode={query.facetMode} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.files} | |||
files={query.files} | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.files} | |||
/> | |||
{displayFilesFacet && | |||
<FileFacet | |||
facetMode={query.facetMode} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.files} | |||
files={query.files} | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.files} | |||
/>} | |||
{!this.props.myIssues && | |||
<AssigneeFacet | |||
component={component} |
@@ -56,6 +56,10 @@ export default class StatusFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(status: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[status] : null; | |||
@@ -96,10 +100,11 @@ export default class StatusFacet extends React.PureComponent { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.statuses.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.statuses.length} | |||
/> | |||
{this.props.open && |
@@ -58,6 +58,10 @@ export default class TagFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
handleSearch = (query: string) => { | |||
return searchIssueTags({ ps: 50, q: query }).then(tags => | |||
tags.map(tag => ({ label: tag, value: tag })) | |||
@@ -83,7 +87,7 @@ export default class TagFacet extends React.PureComponent { | |||
); | |||
} | |||
render() { | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
@@ -92,32 +96,45 @@ export default class TagFacet extends React.PureComponent { | |||
const tags = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{tags.map(tag => ( | |||
<FacetItem | |||
active={this.props.tags.includes(tag)} | |||
facetMode={this.props.facetMode} | |||
key={tag} | |||
name={this.renderTag(tag)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(tag)} | |||
value={tag} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderFooter() { | |||
if (!this.props.stats) { | |||
return null; | |||
} | |||
return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />; | |||
} | |||
render() { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.tags.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.tags.length} | |||
/> | |||
{this.props.open && | |||
<FacetItemsList> | |||
{tags.map(tag => ( | |||
<FacetItem | |||
active={this.props.tags.includes(tag)} | |||
facetMode={this.props.facetMode} | |||
key={tag} | |||
name={this.renderTag(tag)} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(tag)} | |||
value={tag} | |||
/> | |||
))} | |||
</FacetItemsList>} | |||
{this.props.open && | |||
<FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />} | |||
{this.props.open && this.renderList()} | |||
{this.props.open && this.renderFooter()} | |||
</FacetBox> | |||
); | |||
} |
@@ -57,6 +57,10 @@ export default class TypeFacet extends React.PureComponent { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
}; | |||
getStat(type: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[type] : null; | |||
@@ -86,10 +90,11 @@ export default class TypeFacet extends React.PureComponent { | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.types.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.props.types.length} | |||
/> | |||
{this.props.open && |
@@ -42,7 +42,7 @@ it('should render', () => { | |||
expect(renderAssigneeFacet()).toMatchSnapshot(); | |||
}); | |||
it('should not render without stats', () => { | |||
it('should render without stats', () => { | |||
expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot(); | |||
}); | |||
@@ -28,7 +28,7 @@ const renderSidebar = props => | |||
.children() | |||
.map(node => node.name()); | |||
it('should render all facets', () => { | |||
it('should render facets for global page', () => { | |||
expect(renderSidebar()).toMatchSnapshot(); | |||
}); | |||
@@ -1,13 +1,12 @@ | |||
exports[`test should not render without stats 1`] = `null`; | |||
exports[`test should render 1`] = ` | |||
<FacetBox | |||
property="assignees"> | |||
<FacetHeader | |||
hasValue={false} | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} /> | |||
open={true} | |||
values={0} /> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
@@ -62,14 +61,27 @@ exports[`test should render footer select option 1`] = ` | |||
</span> | |||
`; | |||
exports[`test should render without stats 1`] = ` | |||
<FacetBox | |||
property="assignees"> | |||
<FacetHeader | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={0} /> | |||
</FacetBox> | |||
`; | |||
exports[`test should select unassigned 1`] = ` | |||
<FacetBox | |||
property="assignees"> | |||
<FacetHeader | |||
hasValue={true} | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} /> | |||
open={true} | |||
values={1} /> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={true} | |||
@@ -118,10 +130,11 @@ exports[`test should select user 1`] = ` | |||
<FacetBox | |||
property="assignees"> | |||
<FacetHeader | |||
hasValue={true} | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} /> | |||
open={true} | |||
values={1} /> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} |
@@ -1,4 +1,4 @@ | |||
exports[`test should render all facets 1`] = ` | |||
exports[`test should render facets for developer 1`] = ` | |||
Array [ | |||
"FacetMode", | |||
"TypeFacet", | |||
@@ -13,12 +13,11 @@ Array [ | |||
"DirectoryFacet", | |||
"FileFacet", | |||
"AssigneeFacet", | |||
"AuthorFacet", | |||
"LanguageFacet", | |||
] | |||
`; | |||
exports[`test should render facets for developer 1`] = ` | |||
exports[`test should render facets for directory 1`] = ` | |||
Array [ | |||
"FacetMode", | |||
"TypeFacet", | |||
@@ -28,16 +27,14 @@ Array [ | |||
"CreationDateFacet", | |||
"RuleFacet", | |||
"TagFacet", | |||
"ProjectFacet", | |||
"ModuleFacet", | |||
"DirectoryFacet", | |||
"FileFacet", | |||
"AssigneeFacet", | |||
"AuthorFacet", | |||
"LanguageFacet", | |||
] | |||
`; | |||
exports[`test should render facets for directory 1`] = ` | |||
exports[`test should render facets for global page 1`] = ` | |||
Array [ | |||
"FacetMode", | |||
"TypeFacet", | |||
@@ -47,7 +44,7 @@ Array [ | |||
"CreationDateFacet", | |||
"RuleFacet", | |||
"TagFacet", | |||
"FileFacet", | |||
"ProjectFacet", | |||
"AssigneeFacet", | |||
"AuthorFacet", | |||
"LanguageFacet", | |||
@@ -103,9 +100,6 @@ Array [ | |||
"RuleFacet", | |||
"TagFacet", | |||
"ProjectFacet", | |||
"ModuleFacet", | |||
"DirectoryFacet", | |||
"FileFacet", | |||
"AuthorFacet", | |||
"LanguageFacet", | |||
] |
@@ -20,25 +20,34 @@ | |||
// @flow | |||
/* eslint-disable max-len */ | |||
import React from 'react'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
type Props = { | |||
hasValue: boolean, | |||
type Props = {| | |||
name: string, | |||
onClear?: () => void, | |||
onClick?: () => void, | |||
open: boolean | |||
}; | |||
open: boolean, | |||
values?: number | |||
|}; | |||
export default class FacetHeader extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
hasValue: false, | |||
open: true | |||
}; | |||
handleClick = (e: Event & { currentTarget: HTMLElement }) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
handleClearClick = (event: Event & { currentTarget: HTMLElement }) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
if (this.props.onClear) { | |||
this.props.onClear(); | |||
} | |||
}; | |||
handleClick = (event: Event & { currentTarget: HTMLElement }) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
if (this.props.onClick) { | |||
this.props.onClick(); | |||
} | |||
@@ -61,23 +70,32 @@ export default class FacetHeader extends React.PureComponent { | |||
} | |||
renderValueIndicator() { | |||
return this.props.hasValue && !this.props.open | |||
? <svg viewBox="0 0 1792 1792" width="8" height="8" style={{ paddingTop: 5, paddingLeft: 8 }}> | |||
<path | |||
d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z" | |||
fill="#4b9fd5" | |||
/> | |||
</svg> | |||
: null; | |||
if (this.props.open || !this.props.values) { | |||
return null; | |||
} | |||
return <span className="spacer-left badge is-rounded">{this.props.values}</span>; | |||
} | |||
render() { | |||
return this.props.onClick | |||
? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}> | |||
{this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()} | |||
</a> | |||
: <span className="search-navigator-facet-header"> | |||
{this.props.name} | |||
</span>; | |||
const showClearButton: boolean = !!this.props.values && this.props.onClear != null; | |||
return ( | |||
<div> | |||
{showClearButton && | |||
<button | |||
className="search-navigator-facet-header-button button-small button-red" | |||
onClick={this.handleClearClick}> | |||
{translate('clear')} | |||
</button>} | |||
{this.props.onClick | |||
? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}> | |||
{this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()} | |||
</a> | |||
: <span className="search-navigator-facet-header"> | |||
{this.props.name} | |||
</span>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -25,37 +25,41 @@ import FacetHeader from '../FacetHeader'; | |||
it('should render open facet with value', () => { | |||
expect( | |||
shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />) | |||
shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={1} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render open facet without value', () => { | |||
expect( | |||
shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot(); | |||
}); | |||
it('should render closed facet with value', () => { | |||
expect( | |||
shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={false} />) | |||
shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={1} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render closed facet without value', () => { | |||
expect( | |||
shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot(); | |||
}); | |||
it('should render without link', () => { | |||
expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot(); | |||
expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot(); | |||
}); | |||
it('should call onClick', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />); | |||
click(wrapper.find('a')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
it('should clear', () => { | |||
const onClear = jest.fn(); | |||
const wrapper = shallow( | |||
<FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} /> | |||
<FacetHeader name="foo" onClear={onClear} onClick={jest.fn()} open={false} values={3} /> | |||
); | |||
click(wrapper); | |||
expect(onClick).toHaveBeenCalled(); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.button-red')); | |||
expect(onClear).toHaveBeenCalled(); | |||
}); |
@@ -1,132 +1,171 @@ | |||
exports[`test should render closed facet with value 1`] = ` | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"paddingTop": 3, | |||
exports[`test should clear 1`] = ` | |||
<div> | |||
<button | |||
className="search-navigator-facet-header-button button-small button-red" | |||
onClick={[Function]}> | |||
clear | |||
</button> | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"paddingTop": 3, | |||
} | |||
} | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
<span | |||
className="spacer-left badge is-rounded"> | |||
3 | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`test should render closed facet with value 1`] = ` | |||
<div> | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
"paddingTop": 3, | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
<svg | |||
height="8" | |||
style={ | |||
Object { | |||
"paddingLeft": 8, | |||
"paddingTop": 5, | |||
} | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="8"> | |||
<path | |||
d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z" | |||
fill="#4b9fd5" /> | |||
</svg> | |||
</a> | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
<span | |||
className="spacer-left badge is-rounded"> | |||
1 | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`test should render closed facet without value 1`] = ` | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"paddingTop": 3, | |||
} | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" | |||
<div> | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
"paddingTop": 3, | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
</div> | |||
`; | |||
exports[`test should render open facet with value 1`] = ` | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"paddingTop": 3, | |||
} | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" | |||
<div> | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
"paddingTop": 3, | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
</div> | |||
`; | |||
exports[`test should render open facet without value 1`] = ` | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"paddingTop": 3, | |||
} | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" | |||
<div> | |||
<a | |||
className="search-navigator-facet-header" | |||
href="#" | |||
onClick={[Function]}> | |||
<svg | |||
height="10" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
"paddingTop": 3, | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
} | |||
viewBox="0 0 1792 1792" | |||
width="10"> | |||
<path | |||
d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" | |||
style={ | |||
Object { | |||
"fill": "currentColor ", | |||
} | |||
} /> | |||
</svg> | |||
foo | |||
</a> | |||
</div> | |||
`; | |||
exports[`test should render without link 1`] = ` | |||
<span | |||
className="search-navigator-facet-header"> | |||
foo | |||
</span> | |||
<div> | |||
<span | |||
className="search-navigator-facet-header"> | |||
foo | |||
</span> | |||
</div> | |||
`; |
@@ -39,6 +39,12 @@ | |||
} | |||
} | |||
.issues-main-header-spinner { | |||
margin-left: 1px; | |||
margin-right: 9px; | |||
margin-top: -1px; | |||
} | |||
.concise-issues-list-header, | |||
.concise-issues-list-header-inner { | |||
} | |||
@@ -88,9 +94,11 @@ | |||
transition: background-color 0.3s ease, border-color 0.3s ease; | |||
} | |||
.concise-issue-box:hover, | |||
.concise-issue-box:focus { | |||
.concise-issue-box:hover { | |||
background-color: #ffeaea; | |||
} | |||
.concise-issue-box:focus { | |||
outline: none | |||
} | |||
@@ -102,6 +110,8 @@ | |||
} | |||
.concise-issue-box-message { | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
font-weight: bold; | |||
} | |||
@@ -129,4 +139,21 @@ | |||
display: flex; | |||
align-items: flex-start; | |||
border: none; | |||
} | |||
.issues-filters-header { | |||
margin-bottom: 12px; | |||
padding-bottom: 11px; | |||
border-bottom: 1px solid #e6e6e6; | |||
} | |||
.issues-my-issues-filter { | |||
margin-bottom: 24px; | |||
text-align: center; | |||
} | |||
.issues-page-actions { | |||
display: inline-block; | |||
min-width: 80px; | |||
text-align: right; | |||
} |
@@ -164,6 +164,15 @@ type RawFacet = { | |||
export type Facet = { [string]: number }; | |||
export const mapFacet = (facet: string): string => { | |||
const propertyMapping = { | |||
files: 'fileUuids', | |||
modules: 'moduleUuids', | |||
projects: 'projectUuids' | |||
}; | |||
return propertyMapping[facet] || facet; | |||
}; | |||
export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => { | |||
// for readability purpose | |||
const propertyMapping = { |
@@ -24,11 +24,6 @@ import ProjectsListFooterContainer from './ProjectsListFooterContainer'; | |||
import PageSidebar from './PageSidebar'; | |||
import VisualizationsContainer from '../visualizations/VisualizationsContainer'; | |||
import { parseUrlQuery } from '../store/utils'; | |||
import Page from '../../../components/layout/Page'; | |||
import PageMain from '../../../components/layout/PageMain'; | |||
import PageMainInner from '../../../components/layout/PageMainInner'; | |||
import PageSide from '../../../components/layout/PageSide'; | |||
import PageFilters from '../../../components/layout/PageFilters'; | |||
import '../styles.css'; | |||
export default class AllProjects extends React.PureComponent { | |||
@@ -100,19 +95,23 @@ export default class AllProjects extends React.PureComponent { | |||
const top = this.props.organization ? 95 : 30; | |||
return ( | |||
<Page className="projects-page"> | |||
<PageSide top={top}> | |||
<PageFilters> | |||
<PageSidebar | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/> | |||
</PageFilters> | |||
</PageSide> | |||
<div className="layout-page projects-page"> | |||
<div className="layout-page-side-outer"> | |||
<div className="layout-page-side" style={{ top }}> | |||
<div className="layout-page-side-inner"> | |||
<div className="layout-page-filters"> | |||
<PageSidebar | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<PageMain> | |||
<PageMainInner> | |||
<div className="layout-page-main"> | |||
<div className="layout-page-main-inner"> | |||
<PageHeaderContainer onViewChange={this.handleViewChange} view={view} /> | |||
{view === 'list' && | |||
<ProjectsListContainer | |||
@@ -132,9 +131,9 @@ export default class AllProjects extends React.PureComponent { | |||
sort={query.sort} | |||
visualization={visualization} | |||
/>} | |||
</PageMainInner> | |||
</PageMain> | |||
</Page> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -180,7 +180,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
`.source-line-code[data-line-number="${line}"] .source-line-issue-locations` | |||
); | |||
if (lineElement) { | |||
scrollToElement(lineElement, 125, 75); | |||
scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 }); | |||
} | |||
} | |||
@@ -22,7 +22,7 @@ import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import QualifierIcon from '../shared/QualifierIcon'; | |||
import FavoriteContainer from '../controls/FavoriteContainer'; | |||
import { getProjectUrl, getIssuesUrl } from '../../helpers/urls'; | |||
import { getProjectUrl, getComponentIssuesUrl } from '../../helpers/urls'; | |||
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
@@ -171,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<div className="source-viewer-header-measure"> | |||
<span className="source-viewer-header-measure-value"> | |||
<Link | |||
to={getIssuesUrl({ resolved: 'false', fileUuids: uuid })} | |||
to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid })} | |||
className="source-viewer-header-external-link" | |||
target="_blank"> | |||
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} |
@@ -28,7 +28,6 @@ import LineDuplications from './LineDuplications'; | |||
import LineDuplicationBlock from './LineDuplicationBlock'; | |||
import LineIssuesIndicator from './LineIssuesIndicator'; | |||
import LineCode from './LineCode'; | |||
import { TooltipsContainer } from '../../mixins/tooltips-mixin'; | |||
import type { SourceLine } from '../types'; | |||
import type { LinearIssueLocation } from '../helpers/indexing'; | |||
import type { Issue } from '../../issue/types'; | |||
@@ -99,62 +98,60 @@ export default class Line extends React.PureComponent { | |||
}); | |||
return ( | |||
<TooltipsContainer> | |||
<tr className={className} data-line-number={line.line}> | |||
<LineNumber line={line} onClick={this.props.onClick} /> | |||
<tr className={className} data-line-number={line.line}> | |||
<LineNumber line={line} onClick={this.props.onClick} /> | |||
<LineSCM | |||
line={line} | |||
onClick={this.props.onSCMClick} | |||
previousLine={this.props.previousLine} | |||
/> | |||
{this.props.displayCoverage && | |||
<LineCoverage line={line} onClick={this.props.onCoverageClick} />} | |||
{this.props.displayDuplications && | |||
<LineDuplications line={line} onClick={this.props.loadDuplications} />} | |||
<LineSCM | |||
line={line} | |||
onClick={this.props.onSCMClick} | |||
previousLine={this.props.previousLine} | |||
/> | |||
{times(duplicationsCount).map(index => ( | |||
<LineDuplicationBlock | |||
duplicated={duplications.includes(index)} | |||
index={index} | |||
key={index} | |||
line={this.props.line} | |||
onClick={this.props.onDuplicationClick} | |||
/> | |||
))} | |||
{this.props.displayCoverage && | |||
<LineCoverage line={line} onClick={this.props.onCoverageClick} />} | |||
{this.props.displayIssues && | |||
!this.props.displayAllIssues && | |||
<LineIssuesIndicator | |||
issues={this.props.issues} | |||
line={line} | |||
onClick={this.handleIssuesIndicatorClick} | |||
/>} | |||
{this.props.displayDuplications && | |||
<LineDuplications line={line} onClick={this.props.loadDuplications} />} | |||
{this.props.displayFiltered && | |||
<td className="source-meta source-line-filtered-container" data-line-number={line.line}> | |||
<div className="source-line-bar" /> | |||
</td>} | |||
{times(duplicationsCount).map(index => ( | |||
<LineDuplicationBlock | |||
duplicated={duplications.includes(index)} | |||
index={index} | |||
key={index} | |||
line={this.props.line} | |||
onClick={this.props.onDuplicationClick} | |||
/> | |||
))} | |||
<LineCode | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
highlightedSymbols={this.props.highlightedSymbols} | |||
{this.props.displayIssues && | |||
!this.props.displayAllIssues && | |||
<LineIssuesIndicator | |||
issues={this.props.issues} | |||
issueLocations={this.props.issueLocations} | |||
line={line} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssueSelect={this.props.onIssueSelect} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.props.onSymbolClick} | |||
scroll={this.props.scroll} | |||
secondaryIssueLocations={this.props.secondaryIssueLocations} | |||
selectedIssue={this.props.selectedIssue} | |||
showIssues={this.props.openIssues || this.props.displayAllIssues} | |||
/> | |||
</tr> | |||
</TooltipsContainer> | |||
onClick={this.handleIssuesIndicatorClick} | |||
/>} | |||
{this.props.displayFiltered && | |||
<td className="source-meta source-line-filtered-container" data-line-number={line.line}> | |||
<div className="source-line-bar" /> | |||
</td>} | |||
<LineCode | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
highlightedSymbols={this.props.highlightedSymbols} | |||
issues={this.props.issues} | |||
issueLocations={this.props.issueLocations} | |||
line={line} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssueSelect={this.props.onIssueSelect} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.props.onSymbolClick} | |||
scroll={this.props.scroll} | |||
secondaryIssueLocations={this.props.secondaryIssueLocations} | |||
selectedIssue={this.props.selectedIssue} | |||
showIssues={this.props.openIssues || this.props.displayAllIssues} | |||
/> | |||
</tr> | |||
); | |||
} | |||
} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Tooltip from '../../controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { SourceLine } from '../types'; | |||
@@ -40,21 +41,23 @@ export default class LineCoverage extends React.PureComponent { | |||
const className = | |||
'source-meta source-line-coverage' + | |||
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); | |||
const title = line.coverageStatus != null | |||
? translate('source_viewer.tooltip', line.coverageStatus) | |||
: undefined; | |||
return ( | |||
const cell = ( | |||
<td | |||
className={className} | |||
data-line-number={line.line} | |||
title={title} | |||
data-placement={line.coverageStatus != null ? 'right' : undefined} | |||
data-toggle={line.coverageStatus != null ? 'tooltip' : undefined} | |||
role={line.coverageStatus != null ? 'button' : undefined} | |||
tabIndex={line.coverageStatus != null ? 0 : undefined} | |||
onClick={line.coverageStatus != null ? this.handleClick : undefined}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
return line.coverageStatus != null | |||
? <Tooltip | |||
overlay={translate('source_viewer.tooltip', line.coverageStatus)} | |||
placement="right"> | |||
{cell} | |||
</Tooltip> | |||
: cell; | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Tooltip from '../../controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { SourceLine } from '../types'; | |||
@@ -44,20 +45,23 @@ export default class LineDuplicationBlock extends React.PureComponent { | |||
'source-line-duplicated': duplicated | |||
}); | |||
return ( | |||
const cell = ( | |||
<td | |||
key={index} | |||
className={className} | |||
data-line-number={line.line} | |||
data-index={index} | |||
title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined} | |||
data-placement={duplicated ? 'right' : undefined} | |||
data-toggle={duplicated ? 'tooltip' : undefined} | |||
role={duplicated ? 'button' : undefined} | |||
tabIndex={duplicated ? '0' : undefined} | |||
onClick={duplicated ? this.handleClick : undefined}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
return duplicated | |||
? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right"> | |||
{cell} | |||
</Tooltip> | |||
: cell; | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Tooltip from '../../controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { SourceLine } from '../types'; | |||
@@ -41,19 +42,21 @@ export default class LineDuplications extends React.PureComponent { | |||
const className = classNames('source-meta', 'source-line-duplications', { | |||
'source-line-duplicated': line.duplicated | |||
}); | |||
const title = line.duplicated ? translate('source_viewer.tooltip.duplicated_line') : undefined; | |||
return ( | |||
const cell = ( | |||
<td | |||
className={className} | |||
title={title} | |||
data-placement={line.duplicated ? 'right' : undefined} | |||
data-toggle={line.duplicated ? 'tooltip' : undefined} | |||
role={line.duplicated ? 'button' : undefined} | |||
tabIndex={line.duplicated ? 0 : undefined} | |||
onClick={line.duplicated ? this.handleClick : undefined}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
return line.duplicated | |||
? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right"> | |||
{cell} | |||
</Tooltip> | |||
: cell; | |||
} | |||
} |
@@ -27,7 +27,7 @@ it('render covered line', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
@@ -36,7 +36,7 @@ it('render uncovered line', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
@@ -29,7 +29,7 @@ it('render duplicated line', () => { | |||
<LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
@@ -27,7 +27,7 @@ it('render duplicated line', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
@@ -1,16 +1,17 @@ | |||
exports[`test render covered line 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-covered" | |||
data-line-number={3} | |||
data-placement="right" | |||
data-toggle="tooltip" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
title="source_viewer.tooltip.covered"> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.covered" | |||
placement="right"> | |||
<td | |||
className="source-meta source-line-coverage source-line-covered" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0}> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
</Tooltip> | |||
`; | |||
exports[`test render line with unknown coverage 1`] = ` | |||
@@ -23,16 +24,17 @@ exports[`test render line with unknown coverage 1`] = ` | |||
`; | |||
exports[`test render uncovered line 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-uncovered" | |||
data-line-number={3} | |||
data-placement="right" | |||
data-toggle="tooltip" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
title="source_viewer.tooltip.uncovered"> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.uncovered" | |||
placement="right"> | |||
<td | |||
className="source-meta source-line-coverage source-line-uncovered" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0}> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
</Tooltip> | |||
`; |
@@ -1,17 +1,18 @@ | |||
exports[`test render duplicated line 1`] = ` | |||
<td | |||
className="source-meta source-line-duplications-extra source-line-duplicated" | |||
data-index={1} | |||
data-line-number={3} | |||
data-placement="right" | |||
data-toggle="tooltip" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex="0" | |||
title="source_viewer.tooltip.duplicated_block"> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.duplicated_block" | |||
placement="right"> | |||
<td | |||
className="source-meta source-line-duplications-extra source-line-duplicated" | |||
data-index={1} | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex="0"> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
</Tooltip> | |||
`; | |||
exports[`test render not duplicated line 1`] = ` |
@@ -1,15 +1,16 @@ | |||
exports[`test render duplicated line 1`] = ` | |||
<td | |||
className="source-meta source-line-duplications source-line-duplicated" | |||
data-placement="right" | |||
data-toggle="tooltip" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
title="source_viewer.tooltip.duplicated_line"> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.duplicated_line" | |||
placement="right"> | |||
<td | |||
className="source-meta source-line-duplications source-line-duplicated" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0}> | |||
<div | |||
className="source-line-bar" /> | |||
</td> | |||
</Tooltip> | |||
`; | |||
exports[`test render not duplicated line 1`] = ` |
@@ -0,0 +1,7 @@ | |||
.empty-search { | |||
padding: 60px 0; | |||
border: 1px solid #e6e6e6; | |||
border-radius: 2px; | |||
color: #777; | |||
text-align: center; | |||
} |
@@ -19,18 +19,11 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
import { translate } from '../../helpers/l10n'; | |||
import './EmptySearch.css'; | |||
const EmptySearch = () => ( | |||
<div | |||
className={css({ | |||
padding: '60px 0', | |||
border: '1px solid #e6e6e6', | |||
borderRadius: 2, | |||
textAlign: 'center', | |||
color: '#777' | |||
})}> | |||
<div className="empty-search"> | |||
<h3>{translate('no_results_search')}</h3> | |||
<p className="big-spacer-top">{translate('no_results_search.2')}</p> | |||
</div> |
@@ -36,6 +36,7 @@ type State = { | |||
export default class SelectList extends React.PureComponent { | |||
currentKeyScope: string; | |||
previousFilter: Function; | |||
previousKeyScope: string; | |||
props: Props; | |||
state: State; | |||
@@ -66,9 +67,18 @@ export default class SelectList extends React.PureComponent { | |||
attachShortcuts = () => { | |||
this.previousKeyScope = key.getScope(); | |||
this.previousFilter = key.filter; | |||
this.currentKeyScope = uniqueId('key-scope'); | |||
key.setScope(this.currentKeyScope); | |||
// sometimes there is a *focused* search field next to the SelectList component | |||
// we need to allow shortcuts in this case, but only for the used keys | |||
key.filter = (event: KeyboardEvent & { target: HTMLElement }) => { | |||
const tagName = (event.target || event.srcElement).tagName; | |||
const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; | |||
return [13, 38, 40].includes(event.keyCode) || !isInput; | |||
}; | |||
key('down', this.currentKeyScope, () => { | |||
this.setState(this.selectNextElement); | |||
return false; | |||
@@ -80,7 +90,7 @@ export default class SelectList extends React.PureComponent { | |||
}); | |||
key('return', this.currentKeyScope, () => { | |||
if (this.state.active) { | |||
if (this.state.active != null) { | |||
this.handleSelect(this.state.active); | |||
} | |||
return false; | |||
@@ -90,6 +100,7 @@ export default class SelectList extends React.PureComponent { | |||
detachShortcuts = () => { | |||
key.setScope(this.previousKeyScope); | |||
key.deleteScope(this.currentKeyScope); | |||
key.filter = this.previousFilter; | |||
}; | |||
handleSelect = (item: string) => { |
@@ -44,9 +44,7 @@ export default class Checkbox extends React.PureComponent { | |||
} | |||
render() { | |||
const className = classNames('icon-checkbox', { | |||
// trick to work with glamor | |||
[this.props.className]: true, | |||
const className = classNames('icon-checkbox', this.props.className, { | |||
'icon-checkbox-checked': this.props.checked, | |||
'icon-checkbox-single': this.props.thirdState | |||
}); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import key from 'keymaster'; | |||
import IssueView from './IssueView'; | |||
import { updateIssue } from './actions'; | |||
import { setIssueAssignee } from '../../api/issues'; | |||
@@ -86,11 +87,39 @@ export default class BaseIssue extends React.PureComponent { | |||
} | |||
bindShortcuts() { | |||
document.addEventListener('keypress', this.handleKeyPress); | |||
key('f', 'issues', () => { | |||
this.togglePopup('transition'); | |||
return false; | |||
}); | |||
key('a', 'issues', () => { | |||
this.togglePopup('assign'); | |||
return false; | |||
}); | |||
key('m', 'issues', () => { | |||
this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me'); | |||
return false; | |||
}); | |||
key('i', 'issues', () => { | |||
this.togglePopup('set-severity'); | |||
return false; | |||
}); | |||
key('c', 'issues', () => { | |||
this.togglePopup('comment'); | |||
return false; | |||
}); | |||
key('t', 'issues', () => { | |||
this.togglePopup('edit-tags'); | |||
return false; | |||
}); | |||
} | |||
unbindShortcuts() { | |||
document.removeEventListener('keypress', this.handleKeyPress); | |||
key.unbind('f', 'issues'); | |||
key.unbind('a', 'issues'); | |||
key.unbind('m', 'issues'); | |||
key.unbind('i', 'issues'); | |||
key.unbind('c', 'issues'); | |||
key.unbind('t', 'issues'); | |||
} | |||
togglePopup = (popupName: string, open?: boolean) => { | |||
@@ -118,30 +147,6 @@ export default class BaseIssue extends React.PureComponent { | |||
onFail(this.context.store.dispatch)(error); | |||
}; | |||
handleKeyPress = (e: Object) => { | |||
const tagName = e.target.tagName.toUpperCase(); | |||
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; | |||
if (shouldHandle) { | |||
switch (e.key) { | |||
case 'f': | |||
return this.togglePopup('transition'); | |||
case 'a': | |||
return this.togglePopup('assign'); | |||
case 'm': | |||
return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me'); | |||
case 'p': | |||
return this.togglePopup('plan'); | |||
case 'i': | |||
return this.togglePopup('set-severity'); | |||
case 'c': | |||
return this.togglePopup('comment'); | |||
case 't': | |||
return this.togglePopup('edit-tags'); | |||
} | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<IssueView |
@@ -94,7 +94,10 @@ export default function IssueTitleBar(props: Props) { | |||
<li className="issue-meta"> | |||
{onIssuesPage | |||
? locationsBadge | |||
: <Link onClick={stopPropagation} to={getSingleIssueUrl(issue.key)}> | |||
: <Link | |||
onClick={stopPropagation} | |||
target="_blank" | |||
to={getSingleIssueUrl(issue.key)}> | |||
{locationsBadge} | |||
</Link>} | |||
</li>} | |||
@@ -102,6 +105,7 @@ export default function IssueTitleBar(props: Props) { | |||
<Link | |||
className="js-issue-permalink icon-link" | |||
onClick={stopPropagation} | |||
target="_blank" | |||
to={getSingleIssueUrl(issue.key)} | |||
/> | |||
</li> |
@@ -49,11 +49,13 @@ exports[`test should render the titlebar correctly 1`] = ` | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/issues", | |||
"query": Object { | |||
"issues": "AVsae-CQS-9G3txfbFN2", | |||
"open": "AVsae-CQS-9G3txfbFN2", | |||
}, | |||
} | |||
} /> | |||
@@ -116,11 +118,13 @@ exports[`test should render the titlebar with the filter 1`] = ` | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/issues", | |||
"query": Object { | |||
"issues": "AVsae-CQS-9G3txfbFN2", | |||
"open": "AVsae-CQS-9G3txfbFN2", | |||
}, | |||
} | |||
} /> |
@@ -19,8 +19,6 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import { css } from 'glamor'; | |||
import { debounce, map } from 'lodash'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
@@ -54,7 +52,6 @@ type State = { | |||
}; | |||
const LIST_SIZE = 10; | |||
const USER_MARGIN = css({ marginLeft: '24px' }); | |||
export default class SetAssigneePopup extends React.PureComponent { | |||
defaultUsersArray: Array<User>; | |||
@@ -152,9 +149,8 @@ export default class SetAssigneePopup extends React.PureComponent { | |||
size={16} | |||
/>} | |||
<span | |||
className={classNames('vertical-middle', { | |||
[USER_MARGIN]: !(user.avatar || user.email) | |||
})}> | |||
className="vertical-middle" | |||
style={{ marginLeft: !user.avatar && !user.email ? 24 : undefined }}> | |||
{user.name} | |||
</span> | |||
</SelectListItem> |
@@ -37,6 +37,7 @@ type State = { | |||
const LIST_SIZE = 10; | |||
export default class SetIssueTagsPopup extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
@@ -47,15 +48,22 @@ export default class SetIssueTagsPopup extends React.PureComponent { | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.onSearch(''); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
onSearch = (query: string) => { | |||
searchIssueTags({ | |||
q: query || '', | |||
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) | |||
}).then((tags: Array<string>) => { | |||
this.setState({ searchResult: tags }); | |||
if (this.mounted) { | |||
this.setState({ searchResult: tags }); | |||
} | |||
}, this.props.onFail); | |||
}; | |||
@@ -1,42 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
type Props = { | |||
className?: string, | |||
children?: React.Element<*> | |||
}; | |||
const styles = css({ | |||
display: 'flex', | |||
alignItems: 'stretch', | |||
width: '100%', | |||
flexGrow: 1 | |||
}); | |||
export default function Page({ className, children, ...other }: Props) { | |||
return ( | |||
<div className={styles + (className ? ` ${className}` : '')} {...other}> | |||
{children} | |||
</div> | |||
); | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
type Props = { | |||
children?: React.Element<*> | |||
}; | |||
export default function PageSide(props: Props) { | |||
return ( | |||
<div className={css({ width: 260, padding: 20 })}> | |||
{props.children} | |||
</div> | |||
); | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
type Props = { | |||
children?: React.Element<*> | |||
}; | |||
export default function PageMain(props: Props) { | |||
return ( | |||
<div className={css({ flexGrow: 1, minWidth: 740, padding: 20 })}> | |||
{props.children} | |||
</div> | |||
); | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css } from 'glamor'; | |||
type Props = { | |||
children?: React.Element<*> | |||
}; | |||
export default function PageMainInner(props: Props) { | |||
return ( | |||
<div className={css({ minWidth: 740, maxWidth: 980 })}> | |||
{props.children} | |||
</div> | |||
); | |||
} |
@@ -1,73 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { css, media } from 'glamor'; | |||
type Props = { | |||
children?: React.Element<*>, | |||
top?: number | |||
}; | |||
const width = css( | |||
{ | |||
width: 'calc(50vw - 360px)' | |||
}, | |||
media('(max-width: 1320px)', { width: 300 }) | |||
); | |||
const sideStyles = css(width, { | |||
flexGrow: 0, | |||
flexShrink: 0, | |||
backgroundColor: '#f3f3f3' | |||
}); | |||
const sideStickyStyles = css(width, { | |||
position: 'fixed', | |||
zIndex: 40, | |||
top: 0, | |||
bottom: 0, | |||
left: 0, | |||
borderRight: '1px solid #e6e6e6', | |||
overflowY: 'auto', | |||
overflowX: 'hidden', | |||
backgroundColor: '#f3f3f3' | |||
}); | |||
const sideInnerStyles = css( | |||
{ | |||
width: 300, | |||
marginLeft: 'calc(50vw - 660px)', | |||
backgroundColor: '#f3f3f3' | |||
}, | |||
media('(max-width: 1320px)', { marginLeft: 0 }) | |||
); | |||
export default function PageSide(props: Props) { | |||
return ( | |||
<div className={sideStyles}> | |||
<div className={`layout-page-side ${sideStickyStyles}`} style={{ top: props.top || 30 }}> | |||
<div className={sideInnerStyles}> | |||
{props.children} | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -37,13 +37,12 @@ const scrollElement = (element: HTMLElement, position: number) => { | |||
}; | |||
let smoothScrollTop = (y: number, parent) => { | |||
const scrollTop = getScrollPosition(parent); | |||
let scrollTop = getScrollPosition(parent); | |||
const scrollingDown = y > scrollTop; | |||
const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS); | |||
let stepsDone = 0; | |||
const interval = setInterval(() => { | |||
const scrollTop = getScrollPosition(parent); | |||
if (scrollTop === y || SCROLLING_STEPS === stepsDone) { | |||
clearInterval(interval); | |||
} else { | |||
@@ -54,6 +53,7 @@ let smoothScrollTop = (y: number, parent) => { | |||
goal = Math.max(y, scrollTop - step); | |||
} | |||
stepsDone++; | |||
scrollTop = goal; | |||
scrollElement(parent, goal); | |||
} | |||
}, SCROLLING_INTERVAL); | |||
@@ -63,23 +63,41 @@ smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true | |||
export const scrollToElement = ( | |||
element: HTMLElement, | |||
topOffset: number = 0, | |||
bottomOffset: number = 0, | |||
parent: HTMLElement = window | |||
options: { | |||
topOffset?: number, | |||
bottomOffset?: number, | |||
parent?: HTMLElement, | |||
smooth?: boolean | |||
} | |||
) => { | |||
const opts = { topOffset: 0, bottomOffset: 0, parent: window, smooth: true, ...options }; | |||
const { parent } = opts; | |||
const { top, bottom } = element.getBoundingClientRect(); | |||
const scrollTop = getScrollPosition(parent); | |||
const height: number = parent === window | |||
? window.innerHeight | |||
: parent.getBoundingClientRect().height; | |||
const parentTop = parent === window ? 0 : parent.getBoundingClientRect().top; | |||
if (top - parentTop < topOffset) { | |||
smoothScrollTop(scrollTop - topOffset + top - parentTop, parent); | |||
if (top - parentTop < opts.topOffset) { | |||
const goal = scrollTop - opts.topOffset + top - parentTop; | |||
if (opts.smooth) { | |||
smoothScrollTop(goal, parent); | |||
} else { | |||
scrollElement(parent, goal); | |||
} | |||
} | |||
if (bottom - parentTop > height - bottomOffset) { | |||
smoothScrollTop(scrollTop + bottom - parentTop - height + bottomOffset, parent); | |||
if (bottom - parentTop > height - opts.bottomOffset) { | |||
const goal = scrollTop + bottom - parentTop - height + opts.bottomOffset; | |||
if (opts.smooth) { | |||
smoothScrollTop(goal, parent); | |||
} else { | |||
scrollElement(parent, goal); | |||
} | |||
} | |||
}; |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { stringify } from 'querystring'; | |||
import { getProfilePath } from '../apps/quality-profiles/utils'; | |||
/** | |||
@@ -49,11 +50,16 @@ export function getComponentIssuesUrl(componentKey, query) { | |||
return { pathname: '/project/issues', query: { ...query, id: componentKey } }; | |||
} | |||
export function getComponentIssuesUrlAsString(componentKey, query) { | |||
const path = getComponentIssuesUrl(componentKey, query); | |||
return `${window.baseUrl}${path.pathname}?${stringify(path.query)}`; | |||
} | |||
/** | |||
* Generate URL for a single issue | |||
*/ | |||
export function getSingleIssueUrl(issues) { | |||
return { pathname: '/issues', query: { issues } }; | |||
return { pathname: '/issues', query: { issues, open: issues } }; | |||
} | |||
/** |
@@ -26,7 +26,7 @@ | |||
min-width: 10px; | |||
padding: 2px 7px; | |||
font-size: 11px; | |||
font-weight: 300; | |||
font-weight: normal; | |||
letter-spacing: 0.03em; | |||
color: @white; | |||
line-height: 12px; | |||
@@ -41,6 +41,12 @@ | |||
a& { .link-no-underline; } | |||
&.is-rounded { | |||
padding-left: 5px; | |||
padding-right: 5px; | |||
border-radius: 50px; | |||
} | |||
.list-group-item > &, | |||
.list-group-item-heading > & { | |||
float: right; |
@@ -39,6 +39,8 @@ | |||
padding-bottom: @bottomPadding; | |||
border: 1px solid transparent; | |||
background-color: @issueBackgroundColor; | |||
outline: none; | |||
transition: border-color 0.3s ease; | |||
} | |||
.issue-list, |
@@ -174,6 +174,11 @@ ul.modal-head-metadata li { | |||
margin-top: 5px; | |||
margin-bottom: 4px; | |||
} | |||
& > .icon-checkbox { | |||
padding-top: 6px; | |||
padding-right: 8px; | |||
} | |||
} | |||
.modal-field { |
@@ -191,3 +191,66 @@ | |||
} | |||
} | |||
} | |||
.layout-page { | |||
display: flex; | |||
align-items: stretch; | |||
width: 100%; | |||
flex-grow: 1; | |||
} | |||
.layout-page-filters { | |||
width: 260px; | |||
padding: 20px; | |||
} | |||
.layout-page-main { | |||
flex-grow: 1; | |||
min-width: 740px; | |||
padding: 20px; | |||
} | |||
.layout-page-main-inner { | |||
min-width: 740px; | |||
max-width: 980px; | |||
} | |||
.layout-page-side-outer { | |||
width: ~"calc(50vw - 360px)"; | |||
flex-grow: 0; | |||
flex-shrink: 0; | |||
background-color: #f3f3f3; | |||
} | |||
.layout-page-side { | |||
position: fixed; | |||
z-index: 40; | |||
top: 30px; | |||
bottom: 0; | |||
left: 0; | |||
width: ~"calc(50vw - 360px)"; | |||
border-right: 1px solid #e6e6e6; | |||
overflow-y: auto; | |||
overflow-x: hidden; | |||
background-color: #f3f3f3; | |||
} | |||
.layout-page-side-inner { | |||
width: 300px; | |||
margin-left: ~"calc(50vw - 660px)"; | |||
background-color: #f3f3f3; | |||
} | |||
@media (max-width: 1320px) { | |||
.layout-page-side-outer { | |||
width: 300px; | |||
} | |||
.layout-page-side { | |||
width: 300px; | |||
} | |||
.layout-page-side-inner { | |||
margin-left: 0; | |||
} | |||
} |
@@ -122,6 +122,7 @@ | |||
white-space: normal; | |||
overflow: hidden; | |||
font-size: 0; | |||
cursor: not-allowed; | |||
transition: none; | |||
a& { | |||
@@ -309,6 +310,11 @@ | |||
font-weight: 600; | |||
} | |||
.search-navigator-facet-header-button { | |||
float: right; | |||
margin-top: 6px; | |||
} | |||
.search-navigator-facet-list { | |||
margin: 0 0 0 0; | |||
padding: 0 10px 10px; |
@@ -25,7 +25,7 @@ | |||
.issues { | |||
&.sticky { | |||
.issues-workspace-list, | |||
@@ -58,6 +58,26 @@ | |||
.search-navigator-facet-footer { | |||
padding: 0 0 10px 0; | |||
} | |||
.issue-list { | |||
/* no math, just a good guess */ | |||
min-width: 640px; | |||
width: 800px; | |||
@media (max-width: 1320px) { | |||
& { | |||
width: ~"calc(60vw - 40px)"; | |||
} | |||
} | |||
} | |||
.issue { | |||
cursor: pointer; | |||
&:hover { | |||
border-color: @issueBorderColor; | |||
} | |||
} | |||
} | |||
.issues-workspace-list-component { |
@@ -814,16 +814,16 @@ babel-register@^6.22.0: | |||
mkdirp "^0.5.1" | |||
source-map-support "^0.4.2" | |||
babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.9.0: | |||
version "6.22.0" | |||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611" | |||
babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.23.0, babel-runtime@^6.9.0: | |||
version "6.23.0" | |||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" | |||
dependencies: | |||
core-js "^2.4.0" | |||
regenerator-runtime "^0.10.0" | |||
babel-runtime@^6.23.0: | |||
version "6.23.0" | |||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" | |||
babel-runtime@^6.11.6, babel-runtime@^6.22.0: | |||
version "6.22.0" | |||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611" | |||
dependencies: | |||
core-js "^2.4.0" | |||
regenerator-runtime "^0.10.0" | |||
@@ -2381,7 +2381,7 @@ fbjs@0.1.0-alpha.10: | |||
promise "^7.0.3" | |||
whatwg-fetch "^0.9.0" | |||
fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8: | |||
fbjs@^0.8.1, fbjs@^0.8.4: | |||
version "0.8.8" | |||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6" | |||
dependencies: | |||
@@ -2631,14 +2631,6 @@ getpass@^0.1.1: | |||
dependencies: | |||
assert-plus "^1.0.0" | |||
glamor@2.20.24: | |||
version "2.20.24" | |||
resolved "https://repox.sonarsource.com/api/npm/npm/glamor/-/glamor-2.20.24.tgz#a299af2eec687322634ba38e4a0854d8743d2041" | |||
dependencies: | |||
babel-runtime "^6.18.0" | |||
fbjs "^0.8.8" | |||
object-assign "^4.1.0" | |||
glob-base@^0.3.0: | |||
version "0.3.0" | |||
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" |
@@ -227,6 +227,7 @@ bulk_change=Bulk Change | |||
bulleted_point=Bulleted point | |||
check_project=Check project | |||
coding_rules=Rules | |||
clear=Clear | |||
clear_all_filters=Clear All Filters | |||
click_to_add_to_favorites=Click to add to favorites | |||
click_to_remove_from_favorites=Click to remove from favorites | |||
@@ -675,7 +676,7 @@ issue.effort=Effort: | |||
issue.x_effort={0} effort | |||
issue.creation_date=Created | |||
issue.filter_similar_issues=Filter Similar Issues | |||
issue.this_issue_involves_x_code_locations=This issue involved {0} code locations | |||
issue.this_issue_involves_x_code_locations=This issue involves {0} code location(s) | |||
issues.return_to_list=Return to List | |||
issues.issues_limit_reached=For usability reasons, only the {0} issues are displayed. | |||
issues.bulk_change=All Issues ({0}) | |||
@@ -695,6 +696,7 @@ issues.issues=issues | |||
issues.to_select_issues=to select issues | |||
issues.to_navigate=to navigate | |||
issues.to_navigate_issue_locations=to navigate issue locations | |||
issues.to_switch_flows=to switch flows | |||
issues.leak_period=Leak Period | |||
issues.my_issues=My Issues | |||