"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",
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 (
<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>
<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>
<div class="modal-foot">
<a class="js-modal-close" href="#">{{t 'close'}}</a>
-</div>
+</div>
\ No newline at end of file
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,
...project,
name: projectBase != null ? projectBase.longName : '',
issuesUrl: projectBase != null &&
- getComponentIssuesUrl(projectBase.key, {
+ getComponentIssuesUrlAsString(projectBase.key, {
resolved: 'false',
rules: this.model.id
})
// @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
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 };
+ }
+};
areQueriesEqual,
getOpen,
serializeQuery,
- parseFacets
+ parseFacets,
+ mapFacet
} from '../utils';
import type {
Query,
} 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';
// 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();
}
};
open: undefined
}
});
+ this.scrollToSelectedIssue(false);
}
};
}
};
- 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
};
});
};
+ 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);
this.setState(state => ({
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
}));
+ if (!this.state.facets[property]) {
+ this.fetchFacet(property);
+ }
};
handleReset = () => {
this.closeBulkChange();
};
+ handleReload = () => {
+ this.fetchFirstIssues();
+ };
+
handleReloadAndOpenFirst = () => {
this.fetchFirstIssues().then(issues => {
if (issues.length > 0) {
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;
const { query } = this.state;
return (
- <PageFilters>
+ <div className="layout-page-filters">
{currentUser.isLoggedIn &&
<MyIssuesFilter
myIssues={this.state.myIssues}
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
- </PageFilters>
+ </div>
);
}
const { issues, paging } = this.state;
return (
- <PageFilters>
+ <div className="layout-page-filters">
<ConciseIssuesListHeader
loading={this.state.loading}
onBackClick={this.closeIssue}
{paging != null &&
paging.total > 0 &&
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
- </PageFilters>
+ </div>
);
}
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}
}
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>
);
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>
);
}
}
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';
);
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) => (
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} />}
*/
// @flow
import React from 'react';
-import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';
type Props = {
onReset: () => void
};
-const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });
-
export default class FiltersHeader extends React.PureComponent {
props: Props;
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>
}
}
- 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() {
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}
*/
// @flow
import React from 'react';
-import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';
type Props = {|
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}
*/
// @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
|};
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>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ className?: string,
+ onClick: () => void
+|};
+
+/* eslint-disable max-len */
+const icon = (
+ <svg width="18" height="24" viewBox="0 0 18 24">
+ <path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
+ </svg>
+);
+/* eslint-enable max-len */
+
+export default function ReloadButton(props: Props) {
+ const handleClick = (event: Event) => {
+ event.preventDefault();
+ props.onClick();
+ };
+
+ return (
+ <Tooltip overlay={translate('reload')}>
+ <a
+ className={classNames('concise-issues-list-header-button', props.className)}
+ href="#"
+ onClick={handleClick}>
+ {icon}
+ </a>
+ </Tooltip>
+ );
+}
// @flow
import React from 'react';
import classNames from 'classnames';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
type Props = {|
className?: string,
};
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>
);
}
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);
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">
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 });
}
};
// @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';
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-
-type Props = {|
- className?: string,
- onClick: () => void
-|};
-
-/* eslint-disable max-len */
-const icon = (
- <svg width="18" height="24" viewBox="0 0 18 24">
- <path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
- </svg>
-);
-/* eslint-enable max-len */
-
-export default function ReloadButton(props: Props) {
- const handleClick = (event: Event) => {
- event.preventDefault();
- props.onClick();
- };
-
- return (
- <a
- className={classNames('concise-issues-list-header-button', props.className)}
- href="#"
- onClick={handleClick}>
- {icon}
- </a>
- );
-}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ assigned: true, assignees: [] });
+ };
+
handleSearch = (query: string) => searchAssignees(query, this.props.component);
handleSelect = (assignee: string) => {
);
};
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
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) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.resetTo({});
+ };
+
resetTo = (changes: {}) => {
this.props.onChange({
createdAfter: undefined,
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()}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(directory: string): ?number {
const { stats } = this.props;
return stats ? stats[directory] : null;
);
}
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(file: string): ?number {
const { stats } = this.props;
return stats ? stats[file] : null;
);
}
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
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;
this.props.onChange({ [this.property]: uniq([...languages, language]) });
};
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(module: string): ?number {
const { stats } = this.props;
return stats ? stats[module] : null;
);
}
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
handleSearch = (query: string) => {
const { component } = this.props;
);
};
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
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);
}
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 &&
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
handleSearch = (query: string) => {
const { languages } = this.props;
return searchRules({
return stats ? stats[rule] : null;
}
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(severity: string): ?number {
const { stats } = this.props;
return stats ? stats[severity] : null;
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 &&
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 (
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}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(status: string): ?number {
const { stats } = this.props;
return stats ? stats[status] : null;
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 &&
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 }))
);
}
- render() {
+ renderList() {
const { stats } = this.props;
if (!stats) {
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>
);
}
this.props.onToggle(this.property);
};
+ handleClear = () => {
+ this.props.onChange({ [this.property]: [] });
+ };
+
getStat(type: string): ?number {
const { stats } = this.props;
return stats ? stats[type] : null;
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 &&
expect(renderAssigneeFacet()).toMatchSnapshot();
});
-it('should not render without stats', () => {
+it('should render without stats', () => {
expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot();
});
.children()
.map(node => node.name());
-it('should render all facets', () => {
+it('should render facets for global page', () => {
expect(renderSidebar()).toMatchSnapshot();
});
-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}
</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}
<FacetBox
property="assignees">
<FacetHeader
- hasValue={true}
name="issues.facet.assignees"
+ onClear={[Function]}
onClick={[Function]}
- open={true} />
+ open={true}
+ values={1} />
<FacetItemsList>
<FacetItem
active={false}
-exports[`test should render all facets 1`] = `
+exports[`test should render facets for developer 1`] = `
Array [
"FacetMode",
"TypeFacet",
"DirectoryFacet",
"FileFacet",
"AssigneeFacet",
- "AuthorFacet",
"LanguageFacet",
]
`;
-exports[`test should render facets for developer 1`] = `
+exports[`test should render facets for directory 1`] = `
Array [
"FacetMode",
"TypeFacet",
"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",
"CreationDateFacet",
"RuleFacet",
"TagFacet",
- "FileFacet",
+ "ProjectFacet",
"AssigneeFacet",
"AuthorFacet",
"LanguageFacet",
"RuleFacet",
"TagFacet",
"ProjectFacet",
- "ModuleFacet",
- "DirectoryFacet",
- "FileFacet",
"AuthorFacet",
"LanguageFacet",
]
// @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();
}
}
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>
+ );
}
}
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();
});
-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>
`;
}
}
+.issues-main-header-spinner {
+ margin-left: 1px;
+ margin-right: 9px;
+ margin-top: -1px;
+}
+
.concise-issues-list-header,
.concise-issues-list-header-inner {
}
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
}
}
.concise-issue-box-message {
+ overflow: hidden;
+ text-overflow: ellipsis;
font-weight: bold;
}
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;
}
\ No newline at end of file
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 = {
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 {
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
sort={query.sort}
visualization={visualization}
/>}
- </PageMainInner>
- </PageMain>
- </Page>
+ </div>
+ </div>
+ </div>
);
}
}
`.source-line-code[data-line-number="${line}"] .source-line-issue-locations`
);
if (lineElement) {
- scrollToElement(lineElement, 125, 75);
+ scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 });
}
}
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';
<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}
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';
});
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>
);
}
}
*/
// @flow
import React from 'react';
+import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';
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;
}
}
// @flow
import React from 'react';
import classNames from 'classnames';
+import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';
'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;
}
}
// @flow
import React from 'react';
import classNames from 'classnames';
+import Tooltip from '../../controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import type { SourceLine } from '../types';
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;
}
}
const onClick = jest.fn();
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
- click(wrapper);
+ click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});
const onClick = jest.fn();
const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
- click(wrapper);
+ click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});
<LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} />
);
expect(wrapper).toMatchSnapshot();
- click(wrapper);
+ click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});
const onClick = jest.fn();
const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
- click(wrapper);
+ click(wrapper.find('[tabIndex]'));
expect(onClick).toHaveBeenCalled();
});
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`] = `
`;
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>
`;
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`] = `
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`] = `
--- /dev/null
+.empty-search {
+ padding: 60px 0;
+ border: 1px solid #e6e6e6;
+ border-radius: 2px;
+ color: #777;
+ text-align: center;
+}
\ No newline at end of file
*/
// @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>
export default class SelectList extends React.PureComponent {
currentKeyScope: string;
+ previousFilter: Function;
previousKeyScope: string;
props: Props;
state: State;
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;
});
key('return', this.currentKeyScope, () => {
- if (this.state.active) {
+ if (this.state.active != null) {
this.handleSelect(this.state.active);
}
return false;
detachShortcuts = () => {
key.setScope(this.previousKeyScope);
key.deleteScope(this.currentKeyScope);
+ key.filter = this.previousFilter;
};
handleSelect = (item: string) => {
}
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
});
*/
// @flow
import React from 'react';
+import key from 'keymaster';
import IssueView from './IssueView';
import { updateIssue } from './actions';
import { setIssueAssignee } from '../../api/issues';
}
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) => {
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
<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>}
<Link
className="js-issue-permalink icon-link"
onClick={stopPropagation}
+ target="_blank"
to={getSingleIssueUrl(issue.key)}
/>
</li>
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
+ target="_blank"
to={
Object {
"pathname": "/issues",
"query": Object {
"issues": "AVsae-CQS-9G3txfbFN2",
+ "open": "AVsae-CQS-9G3txfbFN2",
},
}
} />
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
+ target="_blank"
to={
Object {
"pathname": "/issues",
"query": Object {
"issues": "AVsae-CQS-9G3txfbFN2",
+ "open": "AVsae-CQS-9G3txfbFN2",
},
}
} />
*/
// @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';
};
const LIST_SIZE = 10;
-const USER_MARGIN = css({ marginLeft: '24px' });
export default class SetAssigneePopup extends React.PureComponent {
defaultUsersArray: Array<User>;
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>
const LIST_SIZE = 10;
export default class SetIssueTagsPopup extends React.PureComponent {
+ mounted: boolean;
props: Props;
state: State;
}
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);
};
+++ /dev/null
-/*
- * 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>
- );
-}
+++ /dev/null
-/*
- * 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>
- );
-}
+++ /dev/null
-/*
- * 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>
- );
-}
+++ /dev/null
-/*
- * 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>
- );
-}
+++ /dev/null
-/*
- * 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>
- );
-}
};
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 {
goal = Math.max(y, scrollTop - step);
}
stepsDone++;
+ scrollTop = goal;
scrollElement(parent, goal);
}
}, SCROLLING_INTERVAL);
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);
+ }
}
};
* 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';
/**
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 } };
}
/**
min-width: 10px;
padding: 2px 7px;
font-size: 11px;
- font-weight: 300;
+ font-weight: normal;
letter-spacing: 0.03em;
color: @white;
line-height: 12px;
a& { .link-no-underline; }
+ &.is-rounded {
+ padding-left: 5px;
+ padding-right: 5px;
+ border-radius: 50px;
+ }
+
.list-group-item > &,
.list-group-item-heading > & {
float: right;
padding-bottom: @bottomPadding;
border: 1px solid transparent;
background-color: @issueBackgroundColor;
+ outline: none;
+ transition: border-color 0.3s ease;
}
.issue-list,
margin-top: 5px;
margin-bottom: 4px;
}
+
+ & > .icon-checkbox {
+ padding-top: 6px;
+ padding-right: 8px;
+ }
}
.modal-field {
}
}
}
+
+.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;
+ }
+}
\ No newline at end of file
white-space: normal;
overflow: hidden;
font-size: 0;
+ cursor: not-allowed;
transition: none;
a& {
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;
.issues {
-
+
&.sticky {
.issues-workspace-list,
.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 {
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"
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:
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"
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
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})
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