From f6276b3b6fecce2b160ed8bdc62a3e87439249e4 Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Fri, 18 Aug 2017 17:47:37 +0200 Subject: SONAR-9385 SONAR-9436 Replace moment with react-intl --- server/sonar-web/config/webpack.config.js | 1 - server/sonar-web/package.json | 4 +- .../js/app/components/LocalizationContainer.js | 53 ----- .../js/app/components/LocalizationContainer.tsx | 76 +++++++ .../app/components/nav/component/ComponentNav.js | 15 +- .../components/nav/component/ComponentNavMeta.js | 45 ++-- .../component/__tests__/ComponentNavMeta-test.js | 35 --- .../component/__tests__/ComponentNavMeta-test.tsx | 35 +++ .../src/main/js/app/utils/exposeLibraries.js | 2 - .../main/js/apps/account/projects/ProjectCard.tsx | 28 +-- .../account/projects/__tests__/ProjectCard-test.js | 4 +- .../apps/background-tasks/components/DateFilter.js | 18 +- .../js/apps/background-tasks/components/Task.js | 6 +- .../apps/background-tasks/components/TaskDate.js | 44 ---- .../apps/background-tasks/components/TaskDate.tsx | 44 ++++ .../js/apps/background-tasks/components/TaskDay.js | 39 ---- .../apps/background-tasks/components/TaskDay.tsx | 39 ++++ .../src/main/js/apps/background-tasks/types.js | 25 --- .../src/main/js/apps/background-tasks/types.ts | 25 +++ .../components/LeakPeriodLegend.js | 12 +- .../components/MeasureContent.js | 7 +- .../components/__tests__/LeakPeriodLegend-test.js | 6 - .../__snapshots__/LeakPeriodLegend-test.js.snap | 14 +- .../js/apps/issues/sidebar/CreationDateFacet.js | 97 +++++---- .../components/ApplicationLeakPeriodLegend.js | 4 +- .../apps/overview/components/LeakPeriodLegend.js | 37 ++-- .../js/apps/overview/components/OverviewApp.js | 3 +- .../components/__tests__/LeakPeriodLegend-test.js | 12 +- .../ApplicationLeakPeriodLegend-test.js.snap | 6 +- .../__snapshots__/LeakPeriodLegend-test.js.snap | 34 +-- .../src/main/js/apps/overview/events/Analysis.js | 4 +- .../src/main/js/apps/overview/events/Event.js | 20 +- .../apps/overview/events/PreviewGraphTooltips.js | 4 +- .../__tests__/__snapshots__/Analysis-test.js.snap | 4 +- .../__tests__/__snapshots__/Event-test.js.snap | 14 +- .../PreviewGraphTooltips-test.js.snap | 4 +- .../src/main/js/apps/overview/main/CodeSmells.js | 28 ++- .../src/main/js/apps/overview/main/enhance.js | 14 +- .../__tests__/__snapshots__/utils-test.js.snap | 78 +++---- .../apps/projectActivity/__tests__/utils-test.js | 73 +++---- .../projectActivity/components/GraphsHistory.js | 3 +- .../projectActivity/components/GraphsTooltips.js | 4 +- .../components/ProjectActivityAnalysesList.js | 10 +- .../components/ProjectActivityAnalysis.js | 4 +- .../components/ProjectActivityApp.js | 3 +- .../components/ProjectActivityAppContainer.js | 5 +- .../components/ProjectActivityDateInput.js | 10 +- .../__tests__/ProjectActivityAnalysesList-test.js | 17 +- .../__tests__/ProjectActivityDateInput-test.js | 4 +- .../__snapshots__/GraphsTooltips-test.js.snap | 9 +- .../ProjectActivityAnalysesList-test.js.snap | 35 ++- .../ProjectActivityDateInput-test.js.snap | 4 +- .../src/main/js/apps/projectActivity/utils.js | 4 +- .../js/apps/projects/components/ProjectCardLeak.js | 27 +-- .../apps/projects/components/ProjectCardOverall.js | 14 +- .../components/__tests__/ProjectCardLeak-test.js | 8 +- .../__tests__/ProjectCardOverall-test.js | 5 - .../__snapshots__/ProjectCardLeak-test.js.snap | 15 +- .../__snapshots__/ProjectCardOverall-test.js.snap | 8 +- .../apps/quality-profiles/changelog/Changelog.tsx | 8 +- .../changelog/__tests__/Changelog-test.tsx | 2 +- .../quality-profiles/components/ProfileDate.tsx | 12 +- .../apps/quality-profiles/home/EvolutionRules.tsx | 20 +- .../quality-profiles/home/EvolutionStagnant.tsx | 21 +- .../src/main/js/apps/quality-profiles/utils.ts | 12 +- .../main/js/apps/settings/licenses/LicenseRow.js | 4 +- .../settings/licenses/__tests__/LicenseRow-test.js | 2 +- .../src/main/js/components/charts/Timeline.js | 237 --------------------- .../src/main/js/components/controls/DateInput.tsx | 6 +- .../src/main/js/components/intl/DateFormatter.tsx | 41 ++++ .../main/js/components/intl/DateTimeFormatter.tsx | 38 ++++ .../js/components/intl/DateTooltipFormatter.tsx | 45 ++++ .../src/main/js/components/intl/TimeFormatter.tsx | 41 ++++ .../js/components/intl/TimeTooltipFormatter.tsx | 44 ++++ .../components/issue/components/IssueChangelog.js | 27 ++- .../issue/components/IssueCommentLine.js | 4 +- .../components/__tests__/IssueChangelog-test.js | 5 - .../components/__tests__/IssueCommentLine-test.js | 2 - .../__snapshots__/IssueChangelog-test.js.snap | 72 ++++--- .../__snapshots__/IssueCommentLine-test.js.snap | 21 +- .../js/components/issue/popups/ChangelogPopup.js | 6 +- .../issue/popups/__tests__/ChangelogPopup-test.js | 2 - .../__snapshots__/ChangelogPopup-test.js.snap | 8 +- .../src/main/js/components/ui/FormattedDate.js | 50 ----- .../src/main/js/components/widgets/barchart.js | 24 +-- .../src/main/js/helpers/__tests__/dates-test.ts | 69 ++++++ .../src/main/js/helpers/__tests__/l10n-test.js | 75 ------- .../src/main/js/helpers/__tests__/l10n-test.ts | 75 +++++++ .../src/main/js/helpers/__tests__/query-test.js | 3 +- server/sonar-web/src/main/js/helpers/dates.ts | 90 ++++++++ .../sonar-web/src/main/js/helpers/handlebars/d.js | 8 +- .../sonar-web/src/main/js/helpers/handlebars/dt.js | 10 +- .../src/main/js/helpers/handlebars/fromNow.js | 4 +- server/sonar-web/src/main/js/helpers/l10n.js | 144 ------------- server/sonar-web/src/main/js/helpers/l10n.ts | 155 ++++++++++++++ server/sonar-web/src/main/js/helpers/periods.js | 3 +- server/sonar-web/src/main/js/helpers/query.js | 12 +- server/sonar-web/src/main/js/helpers/testUtils.ts | 9 +- server/sonar-web/yarn.lock | 51 ++++- 99 files changed, 1444 insertions(+), 1220 deletions(-) delete mode 100644 server/sonar-web/src/main/js/app/components/LocalizationContainer.js create mode 100644 server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js create mode 100644 server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx delete mode 100644 server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js create mode 100644 server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx delete mode 100644 server/sonar-web/src/main/js/apps/background-tasks/types.js create mode 100644 server/sonar-web/src/main/js/apps/background-tasks/types.ts delete mode 100644 server/sonar-web/src/main/js/components/charts/Timeline.js create mode 100644 server/sonar-web/src/main/js/components/intl/DateFormatter.tsx create mode 100644 server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx create mode 100644 server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx create mode 100644 server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx create mode 100644 server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx delete mode 100644 server/sonar-web/src/main/js/components/ui/FormattedDate.js create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts delete mode 100644 server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts create mode 100644 server/sonar-web/src/main/js/helpers/dates.ts delete mode 100644 server/sonar-web/src/main/js/helpers/l10n.js create mode 100644 server/sonar-web/src/main/js/helpers/l10n.ts (limited to 'server/sonar-web') diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index e98dd7fa32a..c5d08949f53 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -32,7 +32,6 @@ module.exports = ({ production = true, fast = false }) => ({ 'react-dom', 'backbone', 'backbone.marionette', - 'moment', 'handlebars/runtime', './src/main/js/libs/third-party/jquery-ui.js', './src/main/js/libs/third-party/select2.js', diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index e9019ccbe63..d95aba437bc 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -20,16 +20,17 @@ "escape-html": "1.0.3", "handlebars": "2.0.0", "history": "3.3.0", + "intl-relativeformat": "2.0.0", "jquery": "2.2.0", "keymaster": "1.6.2", "lodash": "4.17.4", - "moment": "2.18.1", "numeral": "1.5.3", "rc-tooltip": "3.4.7", "react": "15.6.1", "react-dom": "15.6.1", "react-draggable": "2.2.6", "react-helmet": "5.1.3", + "react-intl": "2.3.0", "react-modal": "2.2.2", "react-redux": "5.0.5", "react-router": "3.0.5", @@ -52,6 +53,7 @@ "@types/react": "16.0.2", "@types/react-dom": "15.5.2", "@types/react-helmet": "5.0.3", + "@types/react-intl": "2.3.1", "@types/react-modal": "2.2.0", "@types/react-redux": "5.0.3", "@types/react-router": "3.0.5", diff --git a/server/sonar-web/src/main/js/app/components/LocalizationContainer.js b/server/sonar-web/src/main/js/app/components/LocalizationContainer.js deleted file mode 100644 index 2144033ef9d..00000000000 --- a/server/sonar-web/src/main/js/app/components/LocalizationContainer.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import GlobalLoading from './GlobalLoading'; -import { requestMessages } from '../../helpers/l10n'; - -export default class LocalizationContainer extends React.PureComponent { - /*:: mounted: boolean; */ - - state = { - loading: true - }; - - componentDidMount() { - this.mounted = true; - requestMessages().then(this.finishLoading, this.finishLoading); - } - - componentWillUnmount() { - this.mounted = false; - } - - finishLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - - render() { - if (this.state.loading) { - return ; - } - return this.props.children; - } -} diff --git a/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx b/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx new file mode 100644 index 00000000000..89e68eee611 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { addLocaleData, IntlProvider, Locale } from 'react-intl'; +import GlobalLoading from './GlobalLoading'; +import { DEFAULT_LANGUAGE, requestMessages } from '../../helpers/l10n'; + +interface Props { + children?: any; +} + +interface State { + loading: boolean; + lang?: string; +} + +export default class LocalizationContainer extends React.PureComponent { + mounted: boolean; + + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + requestMessages().then(this.bundleLoaded, this.bundleLoaded); + } + + componentWillUnmount() { + this.mounted = false; + } + + bundleLoaded = (lang: string) => { + import('react-intl/locale-data/' + (lang || DEFAULT_LANGUAGE)).then( + i => this.updateLang(lang, i), + () => { + import('react-intl/locale-data/en').then(i => this.updateLang(lang, i)); + } + ); + }; + + updateLang = (lang: string, intlBundle: Locale[]) => { + if (this.mounted) { + addLocaleData(intlBundle); + this.setState({ loading: false, lang }); + } + }; + + render() { + if (this.state.loading) { + return ; + } + return ( + + {this.props.children} + + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index 811cf2b7397..0e21abc3301 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -24,7 +24,6 @@ import ComponentNavMeta from './ComponentNavMeta'; import ComponentNavMenu from './ComponentNavMenu'; import RecentHistory from '../../RecentHistory'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; -import { TooltipsContainer } from '../../../../components/mixins/tooltips-mixin'; import { getTasksForComponent } from '../../../../api/ce'; import { STATUSES } from '../../../../apps/background-tasks/constants'; import './ComponentNav.css'; @@ -80,14 +79,12 @@ export default class ComponentNav extends React.PureComponent { breadcrumbs={this.props.component.breadcrumbs} /> - - - + - {' '} - {translate('background_task.status.IN_PROGRESS')} - + } + mouseLeaveDelay={2}> +
  • + {' '} + {translate('background_task.status.IN_PROGRESS')} +
  • +
    ); } else if (props.isPending) { const tooltip = canSeeBackgroundTasks ? translateWithParameters('component_navigation.status.pending.admin', backgroundTasksUrl) : translate('component_navigation.status.pending'); metaList.push( -
  • - {translate('background_task.status.PENDING')} -
  • + } + mouseLeaveDelay={2}> +
  • + {translate('background_task.status.PENDING')} +
  • +
    ); } else if (props.isFailed) { const tooltip = canSeeBackgroundTasks ? translateWithParameters('component_navigation.status.failed.admin', backgroundTasksUrl) : translate('component_navigation.status.failed'); metaList.push( -
  • - - {translate('background_task.status.FAILED')} - -
  • + } + mouseLeaveDelay={2}> +
  • + + {translate('background_task.status.FAILED')} + +
  • +
    ); } - if (props.analysisDate) { metaList.push(
  • - {moment(props.analysisDate).format('LLL')} +
  • ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js deleted file mode 100644 index fd99e281ac9..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import ComponentNavMeta from '../ComponentNavMeta'; - -it('renders incremental badge', () => { - check(true); - check(false); - - function check(incremental) { - expect( - shallow( - - ).find('IncrementalBadge') - ).toHaveLength(incremental ? 1 : 0); - } -}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx new file mode 100644 index 00000000000..e5f0a6ab6fa --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import ComponentNavMeta from '../ComponentNavMeta'; + +it('renders incremental badge', () => { + check(true); + check(false); + + function check(incremental: boolean) { + expect( + shallow( + + ).find('IncrementalBadge') + ).toHaveLength(incremental ? 1 : 0); + } +}); diff --git a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js index 7c458767be0..71635357dac 100644 --- a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js +++ b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import * as ReactRedux from 'react-redux'; import * as ReactRouter from 'react-router'; import Select from 'react-select'; @@ -35,7 +34,6 @@ import DuplicationsRating from '../../components/ui/DuplicationsRating'; import Level from '../../components/ui/Level'; const exposeLibraries = () => { - window.moment = moment; window.ReactRedux = ReactRedux; window.ReactRouter = ReactRouter; window.SonarIcons = icons; diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx index b81b2cf256f..cd5f6d39fea 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx @@ -18,35 +18,39 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; import { sortBy } from 'lodash'; +import { FormattedRelative } from 'react-intl'; import { Link } from 'react-router'; import { Project } from './types'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import Level from '../../../components/ui/Level'; +import Tooltip from '../../../components/controls/Tooltip'; import { translateWithParameters, translate } from '../../../helpers/l10n'; interface Props { project: Project; } -export default function ProjectCard(props: Props) { - const { project } = props; +export default function ProjectCard({ project }: Props) { const isAnalyzed = project.lastAnalysisDate != null; - const analysisMoment = isAnalyzed && moment(project.lastAnalysisDate); const links = sortBy(project.links, 'type'); return (
    + } placement="left" >
    @@ -84,36 +89,34 @@ export default class CreationDateFacet extends React.PureComponent { }; handleBarClick = ( - { - createdAfter, - createdBefore - } /*: { - createdAfter: Object, - createdBefore?: Object + { createdAfter, createdBefore } /*: { + createdAfter: Date, + createdBefore?: Date } */ ) => { this.resetTo({ - createdAfter: createdAfter.format(DATE_FORMAT), - createdBefore: createdBefore && createdBefore.format(DATE_FORMAT) + createdAfter: toShortNotSoISOString(createdAfter), + createdBefore: createdBefore && toShortNotSoISOString(createdBefore) }); }; - handlePeriodChange = (property /*: string */) => (value /*: string */) => { + handlePeriodChange = (property /*: string */, value /*: string */) => { this.props.onChange({ createdAt: undefined, createdInLast: undefined, sinceLeakPeriod: undefined, - [property]: value + [property]: toShortNotSoISOString(new Date(value)) }); }; - handlePeriodClick = (period /*: string */) => { - this.resetTo({ createdInLast: period }); - }; + handlePeriodChangeBefore = (value /*: string */) => + this.handlePeriodChange('createdBefore', value); - handleLeakPeriodClick = () => { - this.resetTo({ sinceLeakPeriod: true }); - }; + handlePeriodChangeAfter = (value /*: string */) => this.handlePeriodChange('createdAfter', value); + + handlePeriodClick = (period /*: string */) => this.resetTo({ createdInLast: period }); + + handleLeakPeriodClick = () => this.resetTo({ sinceLeakPeriod: true }); renderBarChart() { const { createdBefore, stats } = this.props; @@ -128,31 +131,32 @@ export default class CreationDateFacet extends React.PureComponent { return null; } - const data = periods.map((startDate, index) => { - const startMoment = moment(startDate); - const nextStartMoment = - index < periods.length - 1 - ? moment(periods[index + 1]) - : createdBefore ? moment(createdBefore) : undefined; - const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days'); + const { formatDate } = this.context.intl; + const beforeDate = createdBefore ? createdBefore : undefined; + const data = periods.map((start, index) => { + const startDate = new Date(start); + let nextStartDate = index < periods.length - 1 ? periods[index + 1] : beforeDate; + let endDate; + if (nextStartDate) { + nextStartDate = new Date(nextStartDate); + endDate = new Date(nextStartDate); + endDate.setDate(endDate.getDate() - 1); + } let tooltip = - formatMeasure(stats[startDate], 'SHORT_INT') + '
    ' + startMoment.format('LL'); - - if (endMoment) { - const isSameDay = endMoment.diff(startMoment, 'days') <= 1; - if (!isSameDay) { - tooltip += ' – ' + endMoment.format('LL'); - } + formatMeasure(stats[start], 'SHORT_INT') + + '
    ' + + formatDate(startDate, longFormatterOption); + if (endDate && !isSameDay(endDate, startDate)) { + tooltip += ' – ' + formatDate(endDate, longFormatterOption); } return { - createdAfter: startMoment, - createdBefore: nextStartMoment, - startMoment, + createdAfter: startDate, + createdBefore: nextStartDate, tooltip, x: index, - y: stats[startDate] + y: stats[start] }; }); @@ -177,13 +181,12 @@ export default class CreationDateFacet extends React.PureComponent { } renderExactDate() { - const m = moment(this.props.createdAt); return (
    - {m.format('LLL')} +
    - ({m.fromNow()}) +
    ); @@ -191,26 +194,26 @@ export default class CreationDateFacet extends React.PureComponent { renderPeriodSelectors() { const { createdAfter, createdBefore } = this.props; - + const { formatDate } = this.context.intl; return (
    ); } - renderPrefefinedPeriods() { + renderPredefinedPeriods() { const { component, createdInLast, sinceLeakPeriod } = this.props; return (
    @@ -259,7 +262,7 @@ export default class CreationDateFacet extends React.PureComponent { :
    {this.renderBarChart()} {this.renderPeriodSelectors()} - {this.renderPrefefinedPeriods()} + {this.renderPredefinedPeriods()}
    ; } diff --git a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js index 3c9785aa8be..aaca5381cb4 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; import { getApplicationLeak } from '../../../api/application'; import { translate } from '../../../helpers/l10n'; @@ -79,7 +79,7 @@ export default class ApplicationLeakPeriodLegend extends React.Component { ?
      {this.state.leaks.map(leak =>
    • - {leak.projectName}: + {leak.projectName}:
    • )}
    diff --git a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js index 88d26edef84..f932aeb8c8e 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js @@ -19,7 +19,8 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; +import DateFormatter from '../../../components/intl/DateFormatter'; import Tooltip from '../../../components/controls/Tooltip'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; import { translateWithParameters } from '../../../helpers/l10n'; @@ -82,23 +83,33 @@ export default function LeakPeriodLegend({ period } /*: { period: Period } */) { } const leakPeriodDate = getPeriodDate(period); - const momentDate = moment(leakPeriodDate); - const fromNow = momentDate.fromNow(); - const note = ['date'].includes(period.mode) - ? translateWithParameters('overview.last_analysis_x', fromNow) - : translateWithParameters('overview.started_x', fromNow); - const tooltip = ['date'].includes(period.mode) - ? translateWithParameters('overview.last_analysis_on_x', momentDate.format('LL')) - : translateWithParameters('overview.started_on_x', momentDate.format('LL')); - + const tooltip = ( + + {formattedLeakPeriodDate => + + {translateWithParameters( + ['date'].includes(period.mode) + ? 'overview.last_analysis_on_x' + : 'overview.started_on_x', + formattedLeakPeriodDate + )} + } + + ); return (
    {translateWithParameters('overview.leak_period_x', leakPeriodLabel)}
    - - {note} - + + {fromNow => + + {translateWithParameters( + ['date'].includes(period.mode) ? 'overview.last_analysis_x' : 'overview.started_x', + fromNow + )} + } +
    ); diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 11d09882375..800a62d4ccd 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import { uniq } from 'lodash'; -import moment from 'moment'; import QualityGate from '../qualityGate/QualityGate'; import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; @@ -124,7 +123,7 @@ export default class OverviewApp extends React.PureComponent { const history /*: History */ = {}; r.measures.forEach(measure => { const measureHistory = measure.history.map(analysis => ({ - date: moment(analysis.date).toDate(), + date: new Date(analysis.date), value: analysis.value })); history[measure.metric] = measureHistory; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js index 251e20594bc..b0179494ae0 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js @@ -37,7 +37,9 @@ describe('check note', () => { mode: 'date', parameter: '2013-01-01' }; - expect(shallow().find('.note')).toMatchSnapshot(); + expect( + shallow().find('FormattedRelative') + ).toMatchSnapshot(); }); it('version', () => { @@ -46,7 +48,9 @@ describe('check note', () => { mode: 'version', parameter: '0.1' }; - expect(shallow().find('.note')).toMatchSnapshot(); + expect( + shallow().find('FormattedRelative') + ).toMatchSnapshot(); }); it('previous_version', () => { @@ -54,7 +58,7 @@ describe('check note', () => { date: '2013-09-22T00:00:00+0200', mode: 'previous_version' }; - expect(shallow().find('.note')).toMatchSnapshot(); + expect(shallow().find('FormattedRelative')).toHaveLength(1); }); it('previous_analysis', () => { @@ -62,6 +66,6 @@ describe('check note', () => { date: '2013-09-22T00:00:00+0200', mode: 'previous_analysis' }; - expect(shallow().find('.note')).toMatchSnapshot(); + expect(shallow().find('FormattedRelative')).toHaveLength(1); }); }); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap index 217405a6565..cb842597822 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap @@ -28,17 +28,15 @@ exports[`renders 2`] = `
  • Foo : -
  • Bar : -
  • diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap index 397016097b3..44caddb1324 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap @@ -9,33 +9,15 @@ exports[`check note 10 days 1`] = ` `; exports[`check note date 1`] = ` - - overview.last_analysis_x.4 years ago - -`; - -exports[`check note previous_analysis 1`] = ` - - overview.started_x.4 years ago - -`; - -exports[`check note previous_version 1`] = ` - - overview.started_x.4 years ago - + `; exports[`check note version 1`] = ` - - overview.started_x.4 years ago - + `; diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js index ef1b6d37e8d..5a9f7cc7807 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js @@ -21,7 +21,7 @@ import React from 'react'; import { sortBy } from 'lodash'; import Event from './Event'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types'; */ @@ -46,7 +46,7 @@ export default function Analysis(props /*: Props */) {
  • - +
    diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js index be23bc7bc07..bf62a5bd1cc 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Event.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Event.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; -/*:: import type { Event as EventType } from '../../projectActivity/types'; */ import { translate } from '../../../helpers/l10n'; +/*:: import type { Event as EventType } from '../../projectActivity/types'; */ export default function Event(props /*: { event: EventType } */) { const { event } = props; @@ -35,13 +35,17 @@ export default function Event(props /*: { event: EventType } */) { } return ( -
    + {translate('event.category', event.category)}:{' '} - - - {event.name} - - -
    + {event.description + ? + + {event.name} + + + : + {event.name} + } + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js index d4bc3609a6d..c1a898a3bf1 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js @@ -19,7 +19,7 @@ */ import React from 'react'; import BubblePopup from '../../../components/common/BubblePopup'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateFormatter from '../../../components/intl/DateFormatter'; import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent'; /*:: import type { Metric } from '../types'; */ /*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */ @@ -56,7 +56,7 @@ export default class PreviewGraphTooltips extends React.PureComponent {
    - +
    diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap index e37d21f47d9..33bc4e28c18 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap @@ -8,9 +8,9 @@ exports[`should sort the events with version first 1`] = ` className="small little-spacer-bottom" > - diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap index 18c6f76ec73..802c927a96c 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap @@ -9,7 +9,7 @@ exports[`should render a version correctly 1`] = ` `; exports[`should render an event correctly 1`] = ` -
    - - - test - - -
    + + test + + `; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap index 0d0aec9012c..64d9d39a3e4 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap @@ -17,9 +17,9 @@ exports[`should render correctly 1`] = `
    -
    + {formattedAnalysisDate => + + {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} + } + + ); return ( @@ -56,12 +62,16 @@ class CodeSmells extends React.PureComponent { } renderTimelineStartDate() { - const momentDate = moment(this.props.historyStartDate); - const fromNow = momentDate.fromNow(); + if (!this.props.historyStartDate) { + return null; + } return ( - - {translateWithParameters('overview.started_x', fromNow)} - + + {fromNow => + + {translateWithParameters('overview.started_x', fromNow)} + } + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index b4be68adcb1..fb008cabb12 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -19,9 +19,9 @@ */ import React from 'react'; import { Link } from 'react-router'; -import moment from 'moment'; import { DrilldownLink } from '../../../components/shared/drilldown-link'; import BubblesIcon from '../../../components/icons-components/BubblesIcon'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import HistoryIcon from '../../../components/icons-components/HistoryIcon'; import Rating from './../../../components/ui/Rating'; import Timeline from '../components/Timeline'; @@ -157,8 +157,16 @@ export default function enhance(ComposedComponent) { if (isDiffMetric(metric)) { Object.assign(params, { sinceLeakPeriod: 'true' }); } - const formattedAnalysisDate = moment(component.analysisDate).format('LLL'); - const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate); + + const tooltip = ( + + {formattedAnalysisDate => + + {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} + } + + ); + return ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap index 06b8994c23d..1bc54255ce1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap @@ -4,11 +4,11 @@ exports[`generateCoveredLinesMetric should correctly generate covered lines metr Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 88, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 50, }, ], @@ -23,11 +23,11 @@ Array [ Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 88, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 50, }, ], @@ -38,11 +38,11 @@ Array [ Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 100, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 100, }, ], @@ -57,9 +57,9 @@ exports[`getAnalysesByVersionByDay should also filter analysis based on the quer Array [ Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -76,9 +76,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-16": Array [ + "1494892800000": Array [ Object { - "date": 2017-05-16T05:09:59.000Z, + "date": 2017-05-16T07:09:59.000Z, "events": Array [ Object { "category": "VERSION", @@ -105,9 +105,9 @@ exports[`getAnalysesByVersionByDay should also filter analysis based on the quer Array [ Object { "byDay": Object { - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, @@ -118,9 +118,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -131,9 +131,9 @@ Array [ "key": "AVxZtCpH7841nF4RNEMI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [ Object { "category": "VERSION", @@ -160,9 +160,9 @@ exports[`getAnalysesByVersionByDay should correctly map analysis by versions and Array [ Object { "byDay": Object { - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T11:06:10.000Z, + "date": 2017-06-09T13:06:10.000Z, "events": Array [ Object { "category": "VERSION", @@ -173,7 +173,7 @@ Array [ "key": "AVyMjlK1HjR_PLDzRbB9", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, @@ -184,9 +184,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -197,14 +197,14 @@ Array [ "key": "AVxZtCpH7841nF4RNEMI", }, Object { - "date": 2017-05-18T05:17:32.000Z, + "date": 2017-05-18T07:17:32.000Z, "events": Array [], "key": "AVwaa1qkpbBde8B6UhYI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [ Object { "category": "VERSION", @@ -221,9 +221,16 @@ Array [ }, Object { "byDay": Object { - "2017-4-16": Array [ + "1494288000000": Array [ Object { - "date": 2017-05-16T05:09:59.000Z, + "date": 2017-05-09T12:03:59.000Z, + "events": Array [], + "key": "AVvtGF3IY6vCuQNDdwxI", + }, + ], + "1494892800000": Array [ + Object { + "date": 2017-05-16T07:09:59.000Z, "events": Array [ Object { "category": "VERSION", @@ -239,13 +246,6 @@ Array [ "key": "AVwQF7kwl-nNFgFWOJ3V", }, ], - "2017-4-9": Array [ - Object { - "date": 2017-05-09T10:03:59.000Z, - "events": Array [], - "key": "AVvtGF3IY6vCuQNDdwxI", - }, - ], }, "key": "AVyM9oI1HjR_PLDzRciU", "version": "1.0", @@ -257,26 +257,26 @@ exports[`getAnalysesByVersionByDay should create fake version 1`] = ` Array [ Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [], "key": "AVxZtCpH7841nF4RNEMI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T11:06:10.000Z, + "date": 2017-06-09T13:06:10.000Z, "events": Array [], "key": "AVyMjlK1HjR_PLDzRbB9", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyMjlK1HjR_PLDzRbB9", }, diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js index 2ee3b2cfe3d..455c679d2d7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js @@ -19,22 +19,23 @@ */ // @flow import * as utils from '../utils'; +import * as dates from '../../../helpers/dates'; const ANALYSES = [ { key: 'AVyMjlK1HjR_PLDzRbB9', - date: new Date('2017-06-09T13:06:10+0200'), + date: new Date('2017-06-09T13:06:10.000Z'), events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1-SNAPSHOT' }] }, - { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, + { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, { key: 'AVyMjlK1HjR_PLDzRbB9', - date: new Date('2017-06-09T11:12:27+0200'), + date: new Date('2017-06-09T11:12:27.000Z'), events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1' }] }, { key: 'AVxZtCpH7841nF4RNEMI', - date: new Date('2017-05-18T14:13:07+0200'), + date: new Date('2017-05-18T14:13:07.000Z'), events: [ { key: 'AVxZtC-N7841nF4RNEMJ', @@ -43,10 +44,10 @@ const ANALYSES = [ } ] }, - { key: 'AVwaa1qkpbBde8B6UhYI', date: new Date('2017-05-18T07:17:32+0200'), events: [] }, + { key: 'AVwaa1qkpbBde8B6UhYI', date: new Date('2017-05-18T07:17:32.000Z'), events: [] }, { key: 'AVwQF7kwl-nNFgFWOJ3V', - date: new Date('2017-05-16T07:09:59+0200'), + date: new Date('2017-05-16T07:09:59.000Z'), events: [ { key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.0' }, { @@ -56,22 +57,22 @@ const ANALYSES = [ } ] }, - { key: 'AVvtGF3IY6vCuQNDdwxI', date: new Date('2017-05-09T12:03:59+0200'), events: [] } + { key: 'AVvtGF3IY6vCuQNDdwxI', date: new Date('2017-05-09T12:03:59.000Z'), events: [] } ]; const HISTORY = [ { metric: 'lines_to_cover', history: [ - { date: new Date('2017-04-27T08:21:32+0200'), value: '100' }, - { date: new Date('2017-04-30T23:06:24+0200'), value: '100' } + { date: new Date('2017-04-27T08:21:32.000Z'), value: '100' }, + { date: new Date('2017-04-30T23:06:24.000Z'), value: '100' } ] }, { metric: 'uncovered_lines', history: [ - { date: new Date('2017-04-27T08:21:32+0200'), value: '12' }, - { date: new Date('2017-04-30T23:06:24+0200'), value: '50' } + { date: new Date('2017-04-27T08:21:32.000Z'), value: '12' }, + { date: new Date('2017-04-30T23:06:24.000Z'), value: '50' } ] } ]; @@ -83,7 +84,7 @@ const METRICS = [ const QUERY = { category: '', - from: new Date('2017-04-27T08:21:32+0200'), + from: new Date('2017-04-27T08:21:32.000Z'), graph: utils.DEFAULT_GRAPH, project: 'foo', to: undefined, @@ -91,16 +92,6 @@ const QUERY = { customMetrics: ['foo', 'bar', 'baz'] }; -jest.mock('moment', () => date => ({ - startOf: () => { - return { - valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` - }; - }, - toDate: () => new Date(date), - format: format => `Formated.${format}:${date.valueOf()}` -})); - describe('generateCoveredLinesMetric', () => { it('should correctly generate covered lines metric', () => { expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot(); @@ -116,6 +107,12 @@ describe('generateSeries', () => { }); describe('getAnalysesByVersionByDay', () => { + dates.startOfDay = jest.fn(date => { + const startDay = new Date(date); + startDay.setUTCHours(0, 0, 0, 0); + return startDay; + }); + it('should correctly map analysis by versions and by days', () => { expect( utils.getAnalysesByVersionByDay(ANALYSES, { @@ -141,8 +138,8 @@ describe('getAnalysesByVersionByDay', () => { customMetrics: [], graph: utils.DEFAULT_GRAPH, project: 'foo', - to: new Date('2017-06-09T11:12:27+0200'), - from: new Date('2017-05-18T14:13:07+0200') + to: new Date('2017-06-09T11:12:27.000Z'), + from: new Date('2017-05-18T14:13:07.000Z') }) ).toMatchSnapshot(); }); @@ -150,10 +147,10 @@ describe('getAnalysesByVersionByDay', () => { expect( utils.getAnalysesByVersionByDay( [ - { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T13:06:10+0200'), events: [] }, - { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, - { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, - { key: 'AVxZtCpH7841nF4RNEMI', date: new Date('2017-05-18T14:13:07+0200'), events: [] } + { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T13:06:10.000Z'), events: [] }, + { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, + { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, + { key: 'AVxZtCpH7841nF4RNEMI', date: new Date('2017-05-18T14:13:07.000Z'), events: [] } ], { category: '', @@ -208,7 +205,7 @@ describe('parseQuery', () => { it('should parse query with default values', () => { expect( utils.parseQuery({ - from: '2017-04-27T08:21:32+0200', + from: '2017-04-27T08:21:32.000Z', id: 'foo', custom_metrics: 'foo,bar,baz' }) @@ -219,11 +216,11 @@ describe('parseQuery', () => { describe('serializeQuery', () => { it('should serialize query for api request', () => { expect(utils.serializeQuery(QUERY)).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', project: 'foo' }); expect(utils.serializeQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', project: 'foo', category: 'test' }); @@ -233,14 +230,14 @@ describe('serializeQuery', () => { describe('serializeUrlQuery', () => { it('should serialize query for url', () => { expect(utils.serializeUrlQuery(QUERY)).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', id: 'foo', custom_metrics: 'foo,bar,baz' }); expect( utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test', customMetrics: [] }) ).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', id: 'foo', graph: 'coverage', category: 'test' @@ -256,8 +253,8 @@ describe('hasHistoryData', () => { name: 'foo', type: 'INT', data: [ - { x: new Date('2017-04-27T08:21:32+0200'), y: 2 }, - { x: new Date('2017-04-30T23:06:24+0200'), y: 2 } + { x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: new Date('2017-04-30T23:06:24.000Z'), y: 2 } ] } ]) @@ -273,8 +270,8 @@ describe('hasHistoryData', () => { name: 'bar', type: 'INT', data: [ - { x: new Date('2017-04-27T08:21:32+0200'), y: 2 }, - { x: new Date('2017-04-30T23:06:24+0200'), y: 2 } + { x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: new Date('2017-04-30T23:06:24.000Z'), y: 2 } ] } ]) @@ -284,7 +281,7 @@ describe('hasHistoryData', () => { { name: 'bar', type: 'INT', - data: [{ x: new Date('2017-04-27T08:21:32+0200'), y: 2 }] + data: [{ x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }] } ]) ).toBeFalsy(); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js index 4e5be7ea5db..044b1462323 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import moment from 'moment'; import { isEqual, sortBy } from 'lodash'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import GraphHistory from './GraphHistory'; @@ -87,7 +86,7 @@ export default class GraphsHistory extends React.PureComponent { return acc.concat({ className: event.category, name: event.name, - date: moment(analysis.date).toDate() + date: new Date(analysis.date) }); }, []); return sortBy(filteredEvents, 'date'); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js index 4eb0e719a28..320f934e028 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import BubblePopup from '../../../components/common/BubblePopup'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import GraphsTooltipsContent from './GraphsTooltipsContent'; import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; @@ -98,7 +98,7 @@ export default class GraphsTooltips extends React.PureComponent {
    - +
    diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js index 7f69288c33d..7b94b01483c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -20,10 +20,9 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import { throttle } from 'lodash'; import ProjectActivityAnalysis from './ProjectActivityAnalysis'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateFormatter from '../../../components/intl/DateFormatter'; import { translate } from '../../../helpers/l10n'; import { activityQueryChanged, @@ -191,12 +190,9 @@ export default class ProjectActivityAnalysesList extends React.PureComponent { }
      {days.map(day => -
    • +
    • - +
        {version.byDay[day] != null && diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index f52a59b5943..bc17b6a46b6 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -23,7 +23,7 @@ import classNames from 'classnames'; import Events from './Events'; import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis } from '../types'; */ @@ -65,7 +65,7 @@ export default class ProjectActivityAnalysis extends React.PureComponent { role="listitem" tabIndex="0">
        - +
        diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index cdc41b48e99..2de698d4b52 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; -import moment from 'moment'; import ProjectActivityPageHeader from './ProjectActivityPageHeader'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityGraphs from './ProjectActivityGraphs'; @@ -89,7 +88,7 @@ export default function ProjectActivityApp(props /*: Props */) {
        ({ - analyses: analyses.map(analysis => ({ ...analysis, date: moment(analysis.date).toDate() })), + analyses: analyses.map(analysis => ({ ...analysis, date: new Date(analysis.date) })), paging })); }; @@ -187,7 +186,7 @@ class ProjectActivityAppContainer extends React.PureComponent { measures.map(measure => ({ metric: measure.metric, history: measure.history.map(analysis => ({ - date: moment(analysis.date).toDate(), + date: new Date(analysis.date), value: analysis.value })) })), diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js index 1ece1355970..f6f85c29c9b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js @@ -19,8 +19,9 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { intlShape } from 'react-intl'; import DateInput from '../../../components/controls/DateInput'; +import { formatterOption } from '../../../components/intl/DateFormatter'; import { parseAsDate } from '../../../helpers/query'; import { translate } from '../../../helpers/l10n'; /*:: import type { RawQuery } from '../../../helpers/query'; */ @@ -36,13 +37,18 @@ type Props = { export default class ProjectActivityDateInput extends React.PureComponent { /*:: props: Props; */ + static contextTypes = { + intl: intlShape + }; + handleFromDateChange = (from /*: string */) => this.props.onChange({ from: parseAsDate(from) }); handleToDateChange = (to /*: string */) => this.props.onChange({ to: parseAsDate(to) }); handleResetClick = () => this.props.onChange({ from: null, to: null }); - formatDate = (date /*: ?Date */) => (date ? moment(date).format('YYYY-MM-DD') : null); + formatDate = (date /*: ?Date */) => + date ? this.context.intl.formatDate(date, formatterOption) : undefined; render() { return ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js index 30864a995f9..6a0c61741f2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js @@ -20,6 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList'; +import * as dates from '../../../../helpers/dates'; import { DEFAULT_GRAPH } from '../../utils'; const ANALYSES = [ @@ -83,18 +84,14 @@ const DEFAULT_PROPS = { updateQuery: () => {} }; -jest.mock('moment', () => date => ({ - startOf: () => { - return { - valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` - }; - }, - toDate: () => new Date(date), - format: format => `Formated.${format}:${date}` -})); - window.Number = val => val; +dates.startOfDay = jest.fn(date => { + const startDay = new Date(date); + startDay.setUTCHours(0, 0, 0, 0); + return startDay; +}); + it('should render correctly', () => { expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js index fa199664561..6b93eaae296 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from '../../../../helpers/testUtils'; import ProjectActivityDateInput from '../ProjectActivityDateInput'; it('should render correctly the date inputs', () => { expect( - shallow( + shallowWithIntl( -
    -
    -
  • -
    • -
      • -
        • -
          • -
  • - {!isBulkChange && moment(event.date).format('LLL')} + {!isBulkChange && } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx index b28f8e236e0..6272bcc2f12 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx @@ -45,7 +45,7 @@ it('should render events', () => { it('should render event date', () => { const events = [createEvent()]; const changelog = shallow(); - expect(changelog.text()).toContain('2016'); + expect(changelog.find('DateTimeFormatter')).toHaveLength(1); }); it('should render author', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx index 947b15e419c..3841c70ee0f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx @@ -18,7 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; +import { FormattedRelative } from 'react-intl'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -27,9 +29,11 @@ interface Props { export default function ProfileDate({ date }: Props) { return date - ? - {moment(date).fromNow()} - + ? }> + + + + : {translate('never')} ; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx index 84df54ceb49..2119c23ae3e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx @@ -19,17 +19,15 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import * as moment from 'moment'; import { sortBy } from 'lodash'; import { searchRules } from '../../../api/rules'; import { translateWithParameters, translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; +import { toShortNotSoISOString } from '../../../helpers/dates'; import { formatMeasure } from '../../../helpers/measures'; const RULES_LIMIT = 10; -const PERIOD_START_MOMENT = moment().subtract(1, 'year'); - function parseRules(r: any) { const { rules, actives } = r; return rules.map((rule: any) => { @@ -55,8 +53,16 @@ interface State { } export default class EvolutionRules extends React.PureComponent { + periodStartDate: string; mounted: boolean; - state: State = {}; + + constructor(props: Props) { + super(props); + this.state = {}; + const startDate = new Date(); + startDate.setFullYear(startDate.getFullYear() - 1); + this.periodStartDate = toShortNotSoISOString(startDate); + } componentDidMount() { this.mounted = true; @@ -69,7 +75,7 @@ export default class EvolutionRules extends React.PureComponent { loadLatestRules() { const data = { - available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD'), + available_since: this.periodStartDate, s: 'createdAt', asc: false, ps: RULES_LIMIT, @@ -92,9 +98,7 @@ export default class EvolutionRules extends React.PureComponent { } const newRulesUrl = getRulesUrl( - { - available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD') - }, + { available_since: this.periodStartDate }, this.props.organization ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx index 578020e7129..5252d1c1840 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; +import DateFormatter from '../../../components/intl/DateFormatter'; import ProfileLink from '../components/ProfileLink'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { isStagnant } from '../utils'; import { Profile } from '../types'; @@ -29,7 +29,7 @@ interface Props { profiles: Profile[]; } -export default function EvolutionStagnan(props: Props) { +export default function EvolutionStagnant(props: Props) { // TODO filter built-in out const outdated = props.profiles.filter(isStagnant); @@ -60,11 +60,16 @@ export default function EvolutionStagnan(props: Props) { {profile.name} -
    - {profile.languageName} - {', '} - updated on {moment(profile.rulesUpdatedAt).format('LL')} -
    + + {formattedDate => +
    + {translateWithParameters( + 'quality_profiles.x_updated_on_y', + profile.languageName, + formattedDate + )} +
    } +
    )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts index cc7140c0fd4..d058aa01acd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { sortBy } from 'lodash'; -import * as moment from 'moment'; +import { differenceInYears, isValidDate } from '../../helpers/dates'; import { Profile } from './types'; export function sortProfiles(profiles: Profile[]) { @@ -65,8 +65,14 @@ export function createFakeProfile(overrides?: any) { }; } -export function isStagnant(profile: Profile) { - return moment().diff(moment(profile.userUpdatedAt), 'years') >= 1; +export function isStagnant(profile: Profile): boolean { + if (profile.userUpdatedAt) { + const updateDate = new Date(profile.userUpdatedAt); + if (isValidDate(updateDate)) { + return differenceInYears(new Date(), updateDate) >= 1; + } + } + return false; } export const getProfilesPath = (organization: string | null | undefined) => diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js index f8ef6d4c7f0..d6b26fd3a75 100644 --- a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js +++ b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js @@ -19,7 +19,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import moment from 'moment'; +import DateFormatter from '../../../components/intl/DateFormatter'; import LicenseStatus from './LicenseStatus'; import LicenseChangeForm from './LicenseChangeForm'; @@ -50,7 +50,7 @@ export default class LicenseRow extends React.PureComponent {
    {license.expiration != null &&
    - {moment(license.expiration).format('LL')} +
    }
    diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js index 4d7c758d416..94e3d55b633 100644 --- a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js +++ b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js @@ -73,7 +73,7 @@ it('should render expiration', () => { '.js-expiration' ); expect(licenseExpiration.length).toBe(1); - expect(licenseExpiration.text()).toContain('2015'); + expect(licenseExpiration.find('DateFormatter')).toHaveLength(1); }); it('should render invalid expiration', () => { diff --git a/server/sonar-web/src/main/js/components/charts/Timeline.js b/server/sonar-web/src/main/js/components/charts/Timeline.js deleted file mode 100644 index 0896127d099..00000000000 --- a/server/sonar-web/src/main/js/components/charts/Timeline.js +++ /dev/null @@ -1,237 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import $ from 'jquery'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import { extent, max } from 'd3-array'; -import { scaleLinear, scalePoint, scaleTime } from 'd3-scale'; -import { line as d3Line, curveBasis } from 'd3-shape'; -import { ResizeMixin } from '../mixins/resize-mixin'; -import { TooltipsMixin } from '../mixins/tooltips-mixin'; - -const Timeline = createReactClass({ - displayName: 'Timeline', - - propTypes: { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - padding: PropTypes.arrayOf(PropTypes.number), - height: PropTypes.number, - basisCurve: PropTypes.bool - }, - - mixins: [ResizeMixin, TooltipsMixin], - - getDefaultProps() { - return { - padding: [10, 10, 10, 10], - basisCurve: true - }; - }, - - getInitialState() { - return { - width: this.props.width, - height: this.props.height - }; - }, - - getRatingScale(availableHeight) { - return scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]); - }, - - getLevelScale(availableHeight) { - return scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]); - }, - - getYScale(availableHeight) { - if (this.props.metricType === 'RATING') { - return this.getRatingScale(availableHeight); - } else if (this.props.metricType === 'LEVEL') { - return this.getLevelScale(availableHeight); - } else { - return scaleLinear() - .range([availableHeight, 0]) - .domain([0, max(this.props.data, d => d.y || 0)]) - .nice(); - } - }, - - handleEventMouseEnter(event) { - $(`.js-event-circle-${event.date.getTime()}`).tooltip('show'); - }, - - handleEventMouseLeave(event) { - $(`.js-event-circle-${event.date.getTime()}`).tooltip('hide'); - }, - - renderHorizontalGrid(xScale, yScale) { - const hasTicks = typeof yScale.ticks === 'function'; - const ticks = hasTicks ? yScale.ticks(4) : yScale.domain(); - - if (!ticks.length) { - ticks.push(yScale.domain()[1]); - } - - const grid = ticks.map(tick => { - const opts = { - x: xScale.range()[0], - y: yScale(tick) - }; - - return ( - - - {this.props.formatYTick(tick)} - - - - ); - }); - - return ( - - {grid} - - ); - }, - - renderTicks(xScale, yScale) { - const format = xScale.tickFormat(7); - let ticks = xScale.ticks(7); - - ticks = ticks.slice(0, -1).map((tick, index) => { - const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1]; - const x = (xScale(tick) + xScale(nextTick)) / 2; - const y = yScale.range()[0]; - - return ( - - {format(tick)} - - ); - }); - - return ( - - {ticks} - - ); - }, - - renderLeak(xScale, yScale) { - if (!this.props.leakPeriodDate) { - return null; - } - - const yScaleRange = yScale.range(); - const opts = { - x: xScale(this.props.leakPeriodDate), - y: yScaleRange[yScaleRange.length - 1], - width: xScale.range()[1] - xScale(this.props.leakPeriodDate), - height: yScaleRange[0] - yScaleRange[yScaleRange.length - 1], - fill: '#fbf3d5' - }; - - return ; - }, - - renderLine(xScale, yScale) { - const p = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y)); - if (this.props.basisCurve) { - p.curve(curveBasis); - } - return ; - }, - - renderEvents(xScale, yScale) { - const points = this.props.events - .map(event => { - const snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime()); - return { ...event, snapshot }; - }) - .filter(event => event.snapshot) - .map(event => { - const key = `${event.date.getTime()}-${event.snapshot.y}`; - const className = `line-chart-point js-event-circle-${event.date.getTime()}`; - const value = event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'; - const tooltip = [ - `${event.version}`, - `${moment(event.date).format('LL')}`, - `${value}` - ].join('
    '); - return ( - - ); - }); - return ( - - {points} - - ); - }, - - render() { - if (!this.state.width || !this.state.height) { - return
    ; - } - const availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3]; - const availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2]; - const xScale = scaleTime() - .domain(extent(this.props.data, d => d.x || 0)) - .range([0, availableWidth]) - .clamp(true); - const yScale = this.getYScale(availableHeight); - return ( - - - {this.renderLeak(xScale, yScale)} - {this.renderHorizontalGrid(xScale, yScale)} - {this.renderTicks(xScale, yScale)} - {this.renderLine(xScale, yScale)} - {this.renderEvents(xScale, yScale)} - - - ); - } -}); -export default Timeline; diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index a1f4a4e70d9..33f6c7d9cf8 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -45,7 +45,7 @@ export default class DateInput extends React.PureComponent { } componentWillReceiveProps(nextProps: Props) { - if (nextProps.value != null) { + if (nextProps.value != null && this.input) { this.input.value = nextProps.value; } } @@ -63,8 +63,8 @@ export default class DateInput extends React.PureComponent { onSelect: this.handleChange.bind(this) }; - if ($.fn && ($.fn as any).datepicker) { - ($(this.refs.input) as any).datepicker(opts); + if ($.fn && ($.fn as any).datepicker && this.input) { + ($(this.input) as any).datepicker(opts); } } diff --git a/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx new file mode 100644 index 00000000000..0c789489587 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { DateSource, FormattedDate } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; + long?: boolean; +} + +export const formatterOption = { year: 'numeric', month: '2-digit', day: '2-digit' }; + +export const longFormatterOption = { year: 'numeric', month: 'long', day: 'numeric' }; + +export default function DateFormatter({ children, date, long }: Props) { + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx new file mode 100644 index 00000000000..560270c23e6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { DateSource, FormattedDate } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; +} + +export const formatterOption = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' +}; + +export default function DateTimeFormatter({ children, date }: Props) { + return ; +} diff --git a/server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx new file mode 100644 index 00000000000..f8c7e90233a --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DateFormatter from './DateFormatter'; +import DateTimeFormatter from './DateTimeFormatter'; +import Tooltip from '../controls/Tooltip'; + +interface Props { + className?: string; + date: Date | string | number; + placement?: string; +} + +export default function DateTooltipFormatter({ className, date, placement }: Props) { + return ( + + {formattedDate => + } + placement={placement} + mouseEnterDelay={0.5}> + + } + + ); +} diff --git a/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx new file mode 100644 index 00000000000..9823aa8a9ce --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { DateSource, FormattedTime } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; + long?: boolean; +} + +export const formatterOption = { hour: 'numeric', minute: 'numeric' }; + +export const longFormatterOption = { hour: 'numeric', minute: 'numeric', second: 'numeric' }; + +export default function TimeFormatter({ children, date, long }: Props) { + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx b/server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx new file mode 100644 index 00000000000..4ee3edc783e --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import TimeFormatter from './TimeFormatter'; +import Tooltip from '../controls/Tooltip'; + +interface Props { + className?: string; + date: Date | string | number; + placement?: string; +} + +export default function TimeTooltipFormatter({ className, date, placement }: Props) { + return ( + + {formattedTime => + } + placement={placement} + mouseEnterDelay={0.5}> + + } + + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js index 721c83ff7e4..404e395479a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js @@ -19,9 +19,11 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import ChangelogPopup from '../popups/ChangelogPopup'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Tooltip from '../../../components/controls/Tooltip'; /*:: import type { Issue } from '../types'; */ /*:: @@ -47,22 +49,25 @@ export default class IssueChangelog extends React.PureComponent { }; render() { - const momentCreationDate = moment(this.props.creationDate); return ( }> - + } + placement="left" + mouseEnterDelay={0.5}> + + ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js index 54836503b39..51865ad5850 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; import Avatar from '../../../components/ui/Avatar'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import CommentDeletePopup from '../popups/CommentDeletePopup'; @@ -98,7 +98,7 @@ export default class IssueCommentLine extends React.PureComponent { tabIndex={0} />
    - ({moment(comment.createdAt).fromNow()}) +
    {comment.updatable && diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js index 446dc8bcea4..65c2c001f3a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -28,11 +28,6 @@ const issue = { creationDate: '2017-03-01T09:36:01+0100' }; -jest.mock('moment', () => () => ({ - format: () => 'March 1, 2017 9:36 AM', - fromNow: () => 'a month ago' -})); - it('should render correctly', () => { const element = shallow( () => ({ fromNow: () => 'a month ago' })); - it('should render correctly a comment that is not updatable', () => { const element = shallow( - + + + + + + `; @@ -62,19 +74,31 @@ exports[`should render correctly 1`] = ` position="bottomright" togglePopup={[Function]} > - + + + + + + `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap index 9d4ea6fa3aa..a3b7b63d015 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap @@ -42,9 +42,10 @@ exports[`should open the right popups when the buttons are clicked 3`] = `
    - ( - a month ago - ) +
    - ( - a month ago - ) +
    - ( - a month ago - ) +
    - {moment(issue.creationDate).format('LLL')} + {author ? `${translate('created_by')} ${author}` : translate('created')} @@ -96,7 +96,7 @@ export default class ChangelogPopup extends React.PureComponent { {this.state.changelogs.map((item, idx) =>
    - {moment(item.creationDate).format('LLL')} + {item.userName && diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js index 35d5c05b5f2..6c4f9d5977e 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js @@ -21,8 +21,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import ChangelogPopup from '../ChangelogPopup'; -jest.mock('moment', () => () => ({ format: () => 'March 1, 2017 9:36 AM' })); - it('should render the changelog popup correctly', () => { const element = shallow( - March 1, 2017 9:36 AM + - March 1, 2017 9:36 AM + - {m.format(format)} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/widgets/barchart.js b/server/sonar-web/src/main/js/components/widgets/barchart.js index 670068ed228..a75ee83fe44 100644 --- a/server/sonar-web/src/main/js/components/widgets/barchart.js +++ b/server/sonar-web/src/main/js/components/widgets/barchart.js @@ -18,17 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; -import moment from 'moment'; import { max } from 'd3-array'; import { select } from 'd3-selection'; import { scaleLinear, scaleBand } from 'd3-scale'; +import { isSameDay, toNotSoISOString } from '../../helpers/dates'; function trans(left, top) { return `translate(${left}, ${top})`; } -const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; - const defaults = function() { return { height: 140, @@ -53,7 +51,7 @@ $.fn.barchart = function(data) { const options = { ...defaults(), ...$(this).data() }; Object.assign(options, { width: options.width || $(this).width(), - endDate: options.endDate ? moment(options.endDate) : null + endDate: options.endDate ? new Date(options.endDate) : null }); const container = select(this); @@ -93,26 +91,28 @@ $.fn.barchart = function(data) { .attr('width', barWidth) .attr('height', d => Math.floor(yScale(d.count))) .style('cursor', 'pointer') - .attr('data-period-start', d => moment(d.val).format(DATE_FORMAT)) + .attr('data-period-start', d => toNotSoISOString(new Date(d.val))) .attr('data-period-end', (d, i) => { - const ending = i < data.length - 1 ? moment(data[i + 1].val) : options.endDate; + const ending = i < data.length - 1 ? new Date(data[i + 1].val) : options.endDate; if (ending) { - return ending.format(DATE_FORMAT); + return toNotSoISOString(ending); } else { return ''; } }) .attr('title', (d, i) => { - const beginning = moment(d.val); - const ending = - i < data.length - 1 ? moment(data[i + 1].val).subtract(1, 'days') : options.endDate; + const beginning = new Date(d.val); + let ending = options.endDate; + if (i < data.length - 1) { + ending = new Date(data[i + 1].val); + ending.setDate(ending.getDate() - 1); + } if (ending) { - const isSameDay = ending.diff(beginning, 'days') <= 1; return ( d.text + '
    ' + beginning.format('LL') + - (isSameDay ? '' : ' – ' + ending.format('LL')) + (isSameDay(ending, beginning) ? '' : ' – ' + ending.format('LL')) ); } else { return d.text + '
    ' + beginning.format('LL'); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts new file mode 100644 index 00000000000..455886d15a8 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as dates from '../dates'; + +const recentDate = new Date('2017-08-16T12:00:00.000Z'); +const recentDate2 = new Date('2016-12-16T12:00:00.000Z'); +const oldDate = new Date('2014-01-12T12:00:00.000Z'); + +it('toShortNotSoISOString', () => { + expect(dates.toShortNotSoISOString(recentDate)).toBe('2017-08-16'); +}); + +it('toNotSoISOString', () => { + expect(dates.toNotSoISOString(recentDate)).toBe('2017-08-16T12:00:00+0000'); +}); + +it('startOfDay', () => { + expect(dates.startOfDay(recentDate).toTimeString()).toContain('00:00:00'); + expect(dates.startOfDay(recentDate)).not.toBe(recentDate); +}); + +it('isValidDate', () => { + expect(dates.isValidDate(recentDate)).toBeTruthy(); + expect(dates.isValidDate(new Date())).toBeTruthy(); + expect(dates.isValidDate(new Date('foo'))).toBeFalsy(); +}); + +it('isSameDay', () => { + expect(dates.isSameDay(recentDate, new Date(recentDate))).toBeTruthy(); + expect(dates.isSameDay(recentDate, recentDate2)).toBeFalsy(); + expect(dates.isSameDay(recentDate, oldDate)).toBeFalsy(); + expect(dates.isSameDay(recentDate, new Date('2016-08-16T12:00:00.000Z'))).toBeFalsy(); +}); + +it('differenceInYears', () => { + expect(dates.differenceInYears(recentDate, recentDate2)).toBe(0); + expect(dates.differenceInYears(recentDate, oldDate)).toBe(3); + expect(dates.differenceInYears(oldDate, recentDate)).toBe(-3); +}); + +it('differenceInDays', () => { + expect(dates.differenceInDays(recentDate, new Date('2017-08-01T12:00:00.000Z'))).toBe(15); + expect(dates.differenceInDays(recentDate, new Date('2017-08-15T23:00:00.000Z'))).toBe(0); + expect(dates.differenceInDays(recentDate, recentDate2)).toBe(243); + expect(dates.differenceInDays(recentDate, oldDate)).toBe(1312); +}); + +it('differenceInSeconds', () => { + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T10:00:00.000Z'))).toBe(7200); + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T12:00:00.500Z'))).toBe(0); + expect(dates.differenceInSeconds(recentDate, oldDate)).toBe(113356800); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js deleted file mode 100644 index 3763be42db6..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { resetBundle, translate, translateWithParameters } from '../l10n'; - -afterEach(() => { - resetBundle({}); -}); - -describe('#translate', () => { - it('should translate simple message', () => { - resetBundle({ my_key: 'my message' }); - expect(translate('my_key')).toBe('my message'); - }); - - it('should translate message with composite key', () => { - resetBundle({ 'my.composite.message': 'my message' }); - expect(translate('my', 'composite', 'message')).toBe('my message'); - expect(translate('my.composite', 'message')).toBe('my message'); - expect(translate('my', 'composite.message')).toBe('my message'); - expect(translate('my.composite.message')).toBe('my message'); - }); - - it('should not translate message but return its key', () => { - expect(translate('random')).toBe('random'); - expect(translate('random', 'key')).toBe('random.key'); - expect(translate('composite.random', 'key')).toBe('composite.random.key'); - }); -}); - -describe('#translateWithParameters', () => { - it('should translate message with one parameter in the beginning', () => { - resetBundle({ x_apples: '{0} apples' }); - expect(translateWithParameters('x_apples', 5)).toBe('5 apples'); - }); - - it('should translate message with one parameter in the middle', () => { - resetBundle({ x_apples: 'I have {0} apples' }); - expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples'); - }); - - it('should translate message with one parameter in the end', () => { - resetBundle({ x_apples: 'Apples: {0}' }); - expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5'); - }); - - it('should translate message with several parameters', () => { - resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' }); - expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe( - '1: I have 3 apples in my 2 baskets - 4' - ); - }); - - it('should not translate message but return its key', () => { - expect(translateWithParameters('random', 5)).toBe('random.5'); - expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3'); - expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2'); - }); -}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts new file mode 100644 index 00000000000..3763be42db6 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { resetBundle, translate, translateWithParameters } from '../l10n'; + +afterEach(() => { + resetBundle({}); +}); + +describe('#translate', () => { + it('should translate simple message', () => { + resetBundle({ my_key: 'my message' }); + expect(translate('my_key')).toBe('my message'); + }); + + it('should translate message with composite key', () => { + resetBundle({ 'my.composite.message': 'my message' }); + expect(translate('my', 'composite', 'message')).toBe('my message'); + expect(translate('my.composite', 'message')).toBe('my message'); + expect(translate('my', 'composite.message')).toBe('my message'); + expect(translate('my.composite.message')).toBe('my message'); + }); + + it('should not translate message but return its key', () => { + expect(translate('random')).toBe('random'); + expect(translate('random', 'key')).toBe('random.key'); + expect(translate('composite.random', 'key')).toBe('composite.random.key'); + }); +}); + +describe('#translateWithParameters', () => { + it('should translate message with one parameter in the beginning', () => { + resetBundle({ x_apples: '{0} apples' }); + expect(translateWithParameters('x_apples', 5)).toBe('5 apples'); + }); + + it('should translate message with one parameter in the middle', () => { + resetBundle({ x_apples: 'I have {0} apples' }); + expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples'); + }); + + it('should translate message with one parameter in the end', () => { + resetBundle({ x_apples: 'Apples: {0}' }); + expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5'); + }); + + it('should translate message with several parameters', () => { + resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' }); + expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe( + '1: I have 3 apples in my 2 baskets - 4' + ); + }); + + it('should not translate message but return its key', () => { + expect(translateWithParameters('random', 5)).toBe('random.5'); + expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3'); + expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2'); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js index 982f9375a36..11d7b289cae 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import * as query from '../query'; describe('queriesEqual', () => { @@ -79,7 +78,7 @@ describe('parseAsDate', () => { }); describe('serializeDate', () => { - const date = moment.utc('2016-06-20T13:09:48.256Z'); + const date = new Date('2016-06-20T13:09:48.256Z'); it('should serialize string correctly', () => { expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000'); expect(query.serializeDate('')).toBeUndefined(); diff --git a/server/sonar-web/src/main/js/helpers/dates.ts b/server/sonar-web/src/main/js/helpers/dates.ts new file mode 100644 index 00000000000..5bbb50b3bff --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/dates.ts @@ -0,0 +1,90 @@ +/* + * 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. + */ + +const MILLISECONDS_IN_MINUTE = 60 * 1000; +const MILLISECONDS_IN_DAY = MILLISECONDS_IN_MINUTE * 60 * 24; + +function pad(number: number) { + if (number < 10) { + return '0' + number; + } + return number; +} + +function compareDateAsc(dateLeft: Date, dateRight: Date): number { + var timeLeft = dateLeft.getTime(); + var timeRight = dateRight.getTime(); + + if (timeLeft < timeRight) { + return -1; + } else if (timeLeft > timeRight) { + return 1; + } else { + return 0; + } +} + +export function toShortNotSoISOString(date: Date): string { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); +} + +export function toNotSoISOString(date: Date): string { + return date.toISOString().replace(/\..+Z$/, '+0000'); +} + +export function startOfDay(date: Date): Date { + const startDay = new Date(date); + startDay.setHours(0, 0, 0, 0); + return startDay; +} + +export function isValidDate(date: Date): boolean { + return !isNaN(date.getTime()); +} + +export function isSameDay(dateLeft: Date, dateRight: Date): boolean { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + return startDateLeft.getTime() === startDateRight.getTime(); +} + +export function differenceInYears(dateLeft: Date, dateRight: Date): number { + const sign = compareDateAsc(dateLeft, dateRight); + const diff = Math.abs(dateLeft.getFullYear() - dateRight.getFullYear()); + const tmpLeftDate = new Date(dateLeft); + tmpLeftDate.setFullYear(dateLeft.getFullYear() - sign * diff); + const isLastYearNotFull = compareDateAsc(tmpLeftDate, dateRight) === -sign; + return sign * (diff - (isLastYearNotFull ? 1 : 0)); +} + +export function differenceInDays(dateLeft: Date, dateRight: Date): number { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + const timestampLeft = + startDateLeft.getTime() - startDateLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + const timestampRight = + startDateRight.getTime() - startDateRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + return Math.round((timestampLeft - timestampRight) / MILLISECONDS_IN_DAY); +} + +export function differenceInSeconds(dateLeft: Date, dateRight: Date): number { + const diff = (dateLeft.getTime() - dateRight.getTime()) / 1000; + return diff > 0 ? Math.floor(diff) : Math.ceil(diff); +} diff --git a/server/sonar-web/src/main/js/helpers/handlebars/d.js b/server/sonar-web/src/main/js/helpers/handlebars/d.js index d457edd9fdb..ef43101b332 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/d.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/d.js @@ -17,8 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/dt.js b/server/sonar-web/src/main/js/helpers/handlebars/dt.js index 708be097e33..3af77ae1d6c 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/dt.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/dt.js @@ -17,8 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LLL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js index dc607b8dca2..ea25726d79f 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); +const IntlRelativeFormat = require('intl-relativeformat'); module.exports = function(date) { - return moment(date).fromNow(); + return new IntlRelativeFormat(localStorage.getItem('l10n.locale') || 'en').format(date); }; diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js deleted file mode 100644 index 1f5ebda6796..00000000000 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -/* @flow */ -import moment from 'moment'; -import { getJSON } from './request'; - -let messages = {}; - -export function translate(...keys /*: string[] */) { - const messageKey = keys.join('.'); - return messages[messageKey] || messageKey; -} - -export function translateWithParameters( - messageKey /*: string */, - ...parameters /*: Array */ -) { - const message = messages[messageKey]; - if (message) { - return parameters - .map(parameter => String(parameter)) - .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message); - } else { - return `${messageKey}.${parameters.join('.')}`; - } -} - -export function hasMessage(...keys /*: string[] */) { - const messageKey = keys.join('.'); - return messages[messageKey] != null; -} - -export function configureMoment(language /*: ?string */) { - moment.locale(language || getPreferredLanguage()); -} - -function getPreferredLanguage() { - return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; -} - -function checkCachedBundle() { - const cached = localStorage.getItem('l10n.bundle'); - - if (!cached) { - return false; - } - - try { - const parsed = JSON.parse(cached); - return parsed != null && typeof parsed === 'object'; - } catch (e) { - return false; - } -} - -function getL10nBundle(params) { - const url = '/api/l10n/index'; - return getJSON(url, params); -} - -export function requestMessages() { - const browserLocale = getPreferredLanguage(); - const cachedLocale = localStorage.getItem('l10n.locale'); - const params = {}; - - if (browserLocale) { - params.locale = browserLocale; - - if (browserLocale.startsWith(cachedLocale)) { - const bundleTimestamp = localStorage.getItem('l10n.timestamp'); - if (bundleTimestamp !== null && checkCachedBundle()) { - params.ts = bundleTimestamp; - } - } - } - - return getL10nBundle(params).then( - ({ effectiveLocale, messages }) => { - try { - const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ'); - localStorage.setItem('l10n.timestamp', currentTimestamp); - localStorage.setItem('l10n.locale', effectiveLocale); - localStorage.setItem('l10n.bundle', JSON.stringify(messages)); - } catch (e) { - // do nothing - } - configureMoment(effectiveLocale); - resetBundle(messages); - }, - ({ response }) => { - if (response && response.status === 304) { - configureMoment(cachedLocale || browserLocale); - resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); - } else { - throw new Error('Unexpected status code: ' + response.status); - } - } - ); -} - -export function resetBundle(bundle /*: Object */) { - messages = bundle; -} - -export function installGlobal() { - window.t = translate; - window.tp = translateWithParameters; - window.requestMessages = requestMessages; -} - -export function getLocalizedDashboardName(baseName /*: string */) { - const l10nKey = `dashboard.${baseName}.name`; - const l10nLabel = translate(l10nKey); - return l10nLabel !== l10nKey ? l10nLabel : baseName; -} - -export function getLocalizedMetricName(metric /*: { key: string, name: string } */) { - const bundleKey = `metric.${metric.key}.name`; - const fromBundle = translate(bundleKey); - return fromBundle !== bundleKey ? fromBundle : metric.name; -} - -export function getLocalizedMetricDomain(domainName /*: string */) { - const bundleKey = `metric_domain.${domainName}`; - const fromBundle = translate(bundleKey); - return fromBundle !== bundleKey ? fromBundle : domainName; -} diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts new file mode 100644 index 00000000000..57f51949074 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getJSON } from './request'; +import { toNotSoISOString } from './dates'; + +interface LanguageBundle { + [name: string]: string; +} + +interface BundleRequestParams { + locale?: string; + ts?: string; +} + +interface BundleRequestResponse { + effectiveLocale: string; + messages: LanguageBundle; +} + +let messages: LanguageBundle = {}; + +export const DEFAULT_LANGUAGE = 'en'; + +export function translate(...keys: string[]): string { + const messageKey = keys.join('.'); + return messages[messageKey] || messageKey; +} + +export function translateWithParameters( + messageKey: string, + ...parameters: Array +): string { + const message = messages[messageKey]; + if (message) { + return parameters + .map(parameter => String(parameter)) + .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message); + } else { + return `${messageKey}.${parameters.join('.')}`; + } +} + +export function hasMessage(...keys: string[]): boolean { + const messageKey = keys.join('.'); + return messages[messageKey] != null; +} + +function getPreferredLanguage(): string | undefined { + return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; +} + +function checkCachedBundle(): boolean { + const cached = localStorage.getItem('l10n.bundle'); + + if (!cached) { + return false; + } + + try { + const parsed = JSON.parse(cached); + return parsed != null && typeof parsed === 'object'; + } catch (e) { + return false; + } +} + +function getL10nBundle(params: BundleRequestParams): Promise { + const url = '/api/l10n/index'; + return getJSON(url, params); +} + +export function requestMessages(): Promise { + const browserLocale = getPreferredLanguage(); + const cachedLocale = localStorage.getItem('l10n.locale'); + const params: BundleRequestParams = {}; + + if (browserLocale) { + params.locale = browserLocale; + + if (cachedLocale && browserLocale.startsWith(cachedLocale)) { + const bundleTimestamp = localStorage.getItem('l10n.timestamp'); + if (bundleTimestamp !== null && checkCachedBundle()) { + params.ts = bundleTimestamp; + } + } + } + + return getL10nBundle(params).then( + ({ effectiveLocale, messages }: BundleRequestResponse) => { + try { + const currentTimestamp = toNotSoISOString(new Date()); + localStorage.setItem('l10n.timestamp', currentTimestamp); + localStorage.setItem('l10n.locale', effectiveLocale); + localStorage.setItem('l10n.bundle', JSON.stringify(messages)); + } catch (e) { + // do nothing + } + resetBundle(messages); + return effectiveLocale || browserLocale || DEFAULT_LANGUAGE; + }, + ({ response }) => { + if (response && response.status === 304) { + resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); + } else { + throw new Error('Unexpected status code: ' + response.status); + } + return cachedLocale || browserLocale || DEFAULT_LANGUAGE; + } + ); +} + +export function resetBundle(bundle: LanguageBundle) { + messages = bundle; +} + +export function installGlobal() { + (window as any).t = translate; + (window as any).tp = translateWithParameters; + (window as any).requestMessages = requestMessages; +} + +export function getLocalizedDashboardName(baseName: string) { + const l10nKey = `dashboard.${baseName}.name`; + const l10nLabel = translate(l10nKey); + return l10nLabel !== l10nKey ? l10nLabel : baseName; +} + +export function getLocalizedMetricName(metric: { key: string; name: string }) { + const bundleKey = `metric.${metric.key}.name`; + const fromBundle = translate(bundleKey); + return fromBundle !== bundleKey ? fromBundle : metric.name; +} + +export function getLocalizedMetricDomain(domainName: string) { + const bundleKey = `metric_domain.${domainName}`; + const fromBundle = translate(bundleKey); + return fromBundle !== bundleKey ? fromBundle : domainName; +} diff --git a/server/sonar-web/src/main/js/helpers/periods.js b/server/sonar-web/src/main/js/helpers/periods.js index 0677d81c13c..4c5ac1c876d 100644 --- a/server/sonar-web/src/main/js/helpers/periods.js +++ b/server/sonar-web/src/main/js/helpers/periods.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import { translate, translateWithParameters } from './l10n'; export function getPeriod(periods, index) { @@ -51,7 +50,7 @@ export function getPeriodDate(period) { return null; } - return moment(period.date).toDate(); + return new Date(period.date); } export function getLeakPeriodLabel(periods) { diff --git a/server/sonar-web/src/main/js/helpers/query.js b/server/sonar-web/src/main/js/helpers/query.js index f7c0f2b6a9c..a87eefe5e3f 100644 --- a/server/sonar-web/src/main/js/helpers/query.js +++ b/server/sonar-web/src/main/js/helpers/query.js @@ -19,7 +19,7 @@ */ // @flow import { isNil, omitBy } from 'lodash'; -import moment from 'moment'; +import { isValidDate, toNotSoISOString } from './dates'; /*:: export type RawQuery = { [string]: any }; @@ -65,9 +65,11 @@ export function parseAsBoolean( } export function parseAsDate(value /*: ?string */) /*: Date | void */ { - const date = moment(value); - if (value && date) { - return date.toDate(); + if (value) { + const date = new Date(value); + if (isValidDate(date)) { + return date; + } } } @@ -85,7 +87,7 @@ export function parseAsArray(value /*: ?string */, itemParser /*: string => * */ export function serializeDate(value /*: ?Date */) /*: string | void */ { if (value != null && value.toISOString) { - return moment(value).format('YYYY-MM-DDTHH:mm:ssZZ'); + return toNotSoISOString(value); } } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index deed3501e74..a0931769355 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ShallowWrapper } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { IntlProvider } from 'react-intl'; export const mockEvent = { target: { blur() {} }, @@ -69,3 +70,9 @@ export function doAsync(fn: Function): Promise { }, 0); }); } + +const intlProvider = new IntlProvider({ locale: 'en' }, {}); +const { intl } = intlProvider.getChildContext(); +export function shallowWithIntl(node, options = {}) { + return shallow(node, { ...options, context: { intl, ...options.context } }); +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index b3a14ca74f1..fc6cfa7afab 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -65,6 +65,10 @@ dependencies: "@types/react" "*" +"@types/react-intl@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.1.tgz#d036dbe54f6ef29f2a150ed303a84f1693ddf905" + "@types/react-modal@2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-2.2.0.tgz#e92bb8454e53030581f263e3fb7e7d27e3eb85b8" @@ -3433,7 +3437,39 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: +intl-format-cache@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.0.5.tgz#b484cefcb9353f374f25de389a3ceea1af18d7c9" + +intl-messageformat-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.2.0.tgz#5906b7f953ab7470e0dc8549097b648b991892ff" + +intl-messageformat@1.3.0, intl-messageformat@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-1.3.0.tgz#f7d926aded7a3ab19b2dc601efd54e99a4bd4eae" + dependencies: + intl-messageformat-parser "1.2.0" + +intl-messageformat@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c" + dependencies: + intl-messageformat-parser "1.2.0" + +intl-relativeformat@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.0.0.tgz#d6ba9dc6c625819bc0abdb1d4e238138b7488f26" + dependencies: + intl-messageformat "^2.0.0" + +intl-relativeformat@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b" + dependencies: + intl-messageformat "1.3.0" + +invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -4550,10 +4586,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" -moment@2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -5673,6 +5705,15 @@ react-input-autosize@^1.1.3: create-react-class "^15.5.2" prop-types "^15.5.8" +react-intl@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.3.0.tgz#e1df6af5667fdf01cbe4aab20e137251e2ae5142" + dependencies: + intl-format-cache "^2.0.5" + intl-messageformat "^1.3.0" + intl-relativeformat "^1.3.0" + invariant "^2.1.1" + react-modal@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.2.2.tgz#4bbf98bc506e61c446c9f57329c7a488ea7d504b" -- cgit v1.2.3