removeSideBarClass,
removeWhitePageClass
} from 'sonar-ui-common/helpers/pages';
+import { serializeDate } from 'sonar-ui-common/helpers/query';
import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import EmptySearch from '../../../components/common/EmptySearch';
}
};
+ createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T'));
+
fetchIssues = (additional: T.RawQuery, requestFacets = false): Promise<FetchIssuesPromise> => {
const { component } = this.props;
const { myIssues, openFacets, query } = this.state;
(component && component.organization) ||
(this.props.organization && this.props.organization.key);
- const parameters = {
+ const parameters: T.Dict<string | undefined> = {
...getBranchLikeQuery(this.props.branchLike),
componentKeys: component && component.key,
s: 'FILE_LINE',
...additional
};
+ if (query.createdAfter !== undefined && this.createdAfterIncludesTime()) {
+ parameters.createdAfter = serializeDate(query.createdAfter);
+ }
+
// only sorting by CREATION_DATE is allowed, so let's sort DESC
if (query.sort) {
Object.assign(parameters, { asc: 'false' });
<Sidebar
branchLike={branchLike}
component={component}
+ createdAfterIncludesTime={this.createdAfterIncludesTime()}
facets={this.state.facets}
hideAuthorFacet={hideAuthorFacet}
loadSearchResultCount={this.loadSearchResultCount}
expect(fetchBranchStatus).toBeCalled();
});
+it('should handle createAfter query param with time', async () => {
+ const fetchIssues = fetchIssuesMockFactory();
+ const wrapper = shallowRender({
+ fetchIssues,
+ location: mockLocation({ query: { createdAfter: '2020-10-21' } })
+ });
+ expect(wrapper.instance().createdAfterIncludesTime()).toBe(false);
+ await waitAndUpdate(wrapper);
+
+ wrapper.setProps({ location: mockLocation({ query: { createdAfter: '2020-10-21T17:21:00Z' } }) });
+ expect(wrapper.instance().createdAfterIncludesTime()).toBe(true);
+
+ fetchIssues.mockClear();
+
+ wrapper.instance().fetchIssues({});
+ expect(fetchIssues).toBeCalledWith(
+ expect.objectContaining({ createdAfter: '2020-10-21T17:21:00+0000' }),
+ false
+ );
+});
+
function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) {
return jest.fn().mockImplementation(({ p }: any) =>
Promise.resolve({
import BarChart from 'sonar-ui-common/components/charts/BarChart';
import { longFormatterOption } from 'sonar-ui-common/components/intl/DateFormatter';
import DateFromNow from 'sonar-ui-common/components/intl/DateFromNow';
-import DateTimeFormatter from 'sonar-ui-common/components/intl/DateTimeFormatter';
+import DateTimeFormatter, {
+ formatterOption as dateTimeFormatterOption
+} from 'sonar-ui-common/components/intl/DateTimeFormatter';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
interface Props {
component: T.Component | undefined;
createdAfter: Date | undefined;
+ createdAfterIncludesTime: boolean;
createdAt: string;
createdBefore: Date | undefined;
createdInLast: string;
stats: T.Dict<number> | undefined;
}
-class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
+export class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
property = 'createdAt';
static defaultProps = {
handleLeakPeriodClick = () => this.resetTo({ sinceLeakPeriod: true });
getValues() {
- const { createdAfter, createdAt, createdBefore, createdInLast, sinceLeakPeriod } = this.props;
+ const {
+ createdAfter,
+ createdAfterIncludesTime,
+ createdAt,
+ createdBefore,
+ createdInLast,
+ sinceLeakPeriod
+ } = this.props;
const { formatDate } = this.props.intl;
const values = [];
if (createdAfter) {
- values.push(formatDate(createdAfter, longFormatterOption));
+ values.push(
+ formatDate(
+ createdAfter,
+ createdAfterIncludesTime ? dateTimeFormatterOption : longFormatterOption
+ )
+ );
}
if (createdAt) {
values.push(formatDate(createdAt, longFormatterOption));
);
}
- renderExactDate() {
- return (
- <div className="search-navigator-facet-container">
- <DateTimeFormatter date={this.props.createdAt} />
- <br />
- <span className="note">
- <DateFromNow date={this.props.createdAt} />
- </span>
- </div>
- );
- }
-
renderPeriodSelectors() {
const { createdAfter, createdBefore } = this.props;
return (
}
renderInner() {
- const { createdAt } = this.props;
- return createdAt ? (
- this.renderExactDate()
- ) : (
+ const { createdAfter, createdAfterIncludesTime, createdAt } = this.props;
+
+ if (createdAt) {
+ return (
+ <div className="search-navigator-facet-container">
+ <DateTimeFormatter date={this.props.createdAt} />
+ <br />
+ <span className="note">
+ <DateFromNow date={this.props.createdAt} />
+ </span>
+ </div>
+ );
+ }
+
+ if (createdAfter && createdAfterIncludesTime) {
+ return (
+ <div className="search-navigator-facet-container">
+ <strong>{translate('after')} </strong>
+ <DateTimeFormatter date={createdAfter} />
+ </div>
+ );
+ }
+
+ return (
<div>
{this.renderBarChart()}
{this.renderPeriodSelectors()}
export interface Props {
branchLike?: BranchLike;
component: T.Component | undefined;
+ createdAfterIncludesTime: boolean;
facets: T.Dict<Facet | undefined>;
hideAuthorFacet?: boolean;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
}
render() {
- const { component, facets, hideAuthorFacet, openFacets, query } = this.props;
+ const {
+ component,
+ createdAfterIncludesTime,
+ facets,
+ hideAuthorFacet,
+ openFacets,
+ query
+ } = this.props;
const displayProjectsFacet =
!component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
<CreationDateFacet
component={component}
createdAfter={query.createdAfter}
+ createdAfterIncludesTime={createdAfterIncludesTime}
createdAt={query.createdAt}
createdBefore={query.createdBefore}
createdInLast={query.createdInLast}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { InjectedIntlProps } from 'react-intl';
+import { mockComponent } from '../../../../helpers/testMocks';
+import { CreationDateFacet } from '../CreationDateFacet';
+
+it('should render correctly', () => {
+ expect(shallowRender({ open: false })).toMatchSnapshot('closed');
+ expect(shallowRender()).toMatchSnapshot('clear');
+ expect(shallowRender({ createdAt: '2019.05.21T13:33:00Z' })).toMatchSnapshot('created at');
+ expect(
+ shallowRender({
+ createdAfter: new Date('2019.04.29T13:33:00Z'),
+ createdAfterIncludesTime: true
+ })
+ ).toMatchSnapshot('created after');
+ expect(
+ shallowRender({
+ createdAfter: new Date('2019.04.29T13:33:00Z'),
+ createdAfterIncludesTime: true
+ })
+ ).toMatchSnapshot('created after timestamp');
+ expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('project');
+});
+
+it.each([
+ ['week', '1w'],
+ ['month', '1m'],
+ ['year', '1y']
+])('should render correctly for createdInLast %s', (_, createdInLast) => {
+ expect(shallowRender({ component: mockComponent(), createdInLast })).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<CreationDateFacet['props']>) {
+ return shallow<CreationDateFacet>(
+ <CreationDateFacet
+ component={undefined}
+ fetching={false}
+ createdAfter={undefined}
+ createdAfterIncludesTime={false}
+ createdAt=""
+ createdBefore={undefined}
+ createdInLast=""
+ sinceLeakPeriod={false}
+ intl={
+ {
+ formatDate: (date: string) => 'formatted.' + date
+ } as InjectedIntlProps['intl']
+ }
+ onChange={jest.fn()}
+ onToggle={jest.fn()}
+ open={true}
+ stats={undefined}
+ {...props}
+ />
+ );
+}
shallow<Sidebar>(
<Sidebar
component={undefined}
+ createdAfterIncludesTime={false}
facets={{}}
loadSearchResultCount={jest.fn()}
loadingFacets={{}}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for createdInLast month 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "issues.facet.createdAt.last_month",
+ ]
+ }
+ />
+ <div>
+ <div
+ className="search-navigator-date-facet-selection"
+ >
+ <DateRangeInput
+ onChange={[Function]}
+ value={
+ Object {
+ "from": undefined,
+ "to": undefined,
+ }
+ }
+ />
+ </div>
+ <div
+ className="spacer-top issues-predefined-periods"
+ >
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.all"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.all"
+ value=""
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.new_code"
+ onClick={[Function]}
+ tooltip="issues.new_code_period"
+ value=""
+ />
+ </div>
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly for createdInLast week 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "issues.facet.createdAt.last_week",
+ ]
+ }
+ />
+ <div>
+ <div
+ className="search-navigator-date-facet-selection"
+ >
+ <DateRangeInput
+ onChange={[Function]}
+ value={
+ Object {
+ "from": undefined,
+ "to": undefined,
+ }
+ }
+ />
+ </div>
+ <div
+ className="spacer-top issues-predefined-periods"
+ >
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.all"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.all"
+ value=""
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.new_code"
+ onClick={[Function]}
+ tooltip="issues.new_code_period"
+ value=""
+ />
+ </div>
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly for createdInLast year 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "issues.facet.createdAt.last_year",
+ ]
+ }
+ />
+ <div>
+ <div
+ className="search-navigator-date-facet-selection"
+ >
+ <DateRangeInput
+ onChange={[Function]}
+ value={
+ Object {
+ "from": undefined,
+ "to": undefined,
+ }
+ }
+ />
+ </div>
+ <div
+ className="spacer-top issues-predefined-periods"
+ >
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.all"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.all"
+ value=""
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.new_code"
+ onClick={[Function]}
+ tooltip="issues.new_code_period"
+ value=""
+ />
+ </div>
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: clear 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={Array []}
+ />
+ <div>
+ <div
+ className="search-navigator-date-facet-selection"
+ >
+ <DateRangeInput
+ onChange={[Function]}
+ value={
+ Object {
+ "from": undefined,
+ "to": undefined,
+ }
+ }
+ />
+ </div>
+ <div
+ className="spacer-top issues-predefined-periods"
+ >
+ <FacetItem
+ active={true}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.all"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.all"
+ value=""
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.last_week"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.last_week"
+ value="1w"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.last_month"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.last_month"
+ value="1m"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.last_year"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.last_year"
+ value="1y"
+ />
+ </div>
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: closed 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={false}
+ values={Array []}
+ />
+</FacetBox>
+`;
+
+exports[`should render correctly: created after 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "formatted.Invalid Date",
+ ]
+ }
+ />
+ <div
+ className="search-navigator-facet-container"
+ >
+ <strong>
+ after
+
+ </strong>
+ <DateTimeFormatter
+ date={Date { NaN }}
+ />
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: created after timestamp 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "formatted.Invalid Date",
+ ]
+ }
+ />
+ <div
+ className="search-navigator-facet-container"
+ >
+ <strong>
+ after
+
+ </strong>
+ <DateTimeFormatter
+ date={Date { NaN }}
+ />
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: created at 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "formatted.2019.05.21T13:33:00Z",
+ ]
+ }
+ />
+ <div
+ className="search-navigator-facet-container"
+ >
+ <DateTimeFormatter
+ date="2019.05.21T13:33:00Z"
+ />
+ <br />
+ <span
+ className="note"
+ >
+ <DateFromNow
+ date="2019.05.21T13:33:00Z"
+ />
+ </span>
+ </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: project 1`] = `
+<FacetBox
+ property="createdAt"
+>
+ <FacetHeader
+ fetching={false}
+ name="issues.facet.createdAt"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={Array []}
+ />
+ <div>
+ <div
+ className="search-navigator-date-facet-selection"
+ >
+ <DateRangeInput
+ onChange={[Function]}
+ value={
+ Object {
+ "from": undefined,
+ "to": undefined,
+ }
+ }
+ />
+ </div>
+ <div
+ className="spacer-top issues-predefined-periods"
+ >
+ <FacetItem
+ active={true}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.facet.createdAt.all"
+ onClick={[Function]}
+ tooltip="issues.facet.createdAt.all"
+ value=""
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ loading={false}
+ name="issues.new_code"
+ onClick={[Function]}
+ tooltip="issues.new_code_period"
+ value=""
+ />
+ </div>
+ </div>
+</FacetBox>
+`;
activate=Activate
add_verb=Add
admin=Admin
+after=After
apply=Apply
all=All
and=And