]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9385 SONAR-9436 Replace moment with react-intl
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 18 Aug 2017 15:47:37 +0000 (17:47 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 25 Aug 2017 09:05:36 +0000 (11:05 +0200)
100 files changed:
server/sonar-web/config/webpack.config.js
server/sonar-web/package.json
server/sonar-web/src/main/js/app/components/LocalizationContainer.js [deleted file]
server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/exposeLibraries.js
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js
server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js
server/sonar-web/src/main/js/apps/background-tasks/components/Task.js
server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/types.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/types.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js
server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap
server/sonar-web/src/main/js/apps/overview/events/Analysis.js
server/sonar-web/src/main/js/apps/overview/events/Event.js
server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js
server/sonar-web/src/main/js/apps/overview/main/enhance.js
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap
server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js
server/sonar-web/src/main/js/components/charts/Timeline.js [deleted file]
server/sonar-web/src/main/js/components/controls/DateInput.tsx
server/sonar-web/src/main/js/components/intl/DateFormatter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
server/sonar-web/src/main/js/components/ui/FormattedDate.js [deleted file]
server/sonar-web/src/main/js/components/widgets/barchart.js
server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js [deleted file]
server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/query-test.js
server/sonar-web/src/main/js/helpers/dates.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/handlebars/d.js
server/sonar-web/src/main/js/helpers/handlebars/dt.js
server/sonar-web/src/main/js/helpers/handlebars/fromNow.js
server/sonar-web/src/main/js/helpers/l10n.js [deleted file]
server/sonar-web/src/main/js/helpers/l10n.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/periods.js
server/sonar-web/src/main/js/helpers/query.js
server/sonar-web/src/main/js/helpers/testUtils.ts
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e98dd7fa32aa9551f78106e68c420bc99295b58c..c5d08949f530ced1eca2a866eda70e20bb3c00bb 100644 (file)
@@ -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',
index e9019ccbe6326aca72d9f8c183709b6f2e21ad4c..d95aba437bc7d1b97fde392709cc595396f40479 100644 (file)
     "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 (file)
index 2144033..0000000
+++ /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 <GlobalLoading />;
-    }
-    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 (file)
index 0000000..89e68ee
--- /dev/null
@@ -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<Props, State> {
+  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 <GlobalLoading />;
+    }
+    return (
+      <IntlProvider
+        locale={this.state.lang || DEFAULT_LANGUAGE}
+        defaultLocale={this.state.lang || DEFAULT_LANGUAGE}>
+        {this.props.children}
+      </IntlProvider>
+    );
+  }
+}
index 811cf2b73976b1d3ff353307e52cd27a212d3ecf..0e21abc3301cb6f110d7818fc6d0bbe19d51f533 100644 (file)
@@ -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}
         />
 
-        <TooltipsContainer options={{ delay: { show: 0, hide: 2000 } }}>
-          <ComponentNavMeta
-            {...this.props}
-            {...this.state}
-            version={this.props.component.version}
-            analysisDate={this.props.component.analysisDate}
-          />
-        </TooltipsContainer>
+        <ComponentNavMeta
+          {...this.props}
+          {...this.state}
+          version={this.props.component.version}
+          analysisDate={this.props.component.analysisDate}
+        />
 
         <ComponentNavMenu
           component={this.props.component}
index 15b086e9b3cb0b5f047eb9384776c9527a8ef506..28133dd41fe81f4f9dac8b73591daa027d05c7d5 100644 (file)
  * 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 React from 'react';
+import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
 import IncrementalBadge from './IncrementalBadge';
 import PendingIcon from '../../../../components/shared/pending-icon';
+import Tooltip from '../../../../components/controls/Tooltip';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 
 export default function ComponentNavMeta(props) {
@@ -34,37 +35,51 @@ export default function ComponentNavMeta(props) {
       ? translateWithParameters('component_navigation.status.in_progress.admin', backgroundTasksUrl)
       : translate('component_navigation.status.in_progress');
     metaList.push(
-      <li key="isInProgress" data-toggle="tooltip" title={tooltip}>
-        <i className="spinner" style={{ marginTop: '-1px' }} />{' '}
-        <span className="text-info">{translate('background_task.status.IN_PROGRESS')}</span>
-      </li>
+      <Tooltip
+        key="isInProgress"
+        overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />}
+        mouseLeaveDelay={2}>
+        <li>
+          <i className="spinner" style={{ marginTop: '-1px' }} />{' '}
+          <span className="text-info">{translate('background_task.status.IN_PROGRESS')}</span>
+        </li>
+      </Tooltip>
     );
   } else if (props.isPending) {
     const tooltip = canSeeBackgroundTasks
       ? translateWithParameters('component_navigation.status.pending.admin', backgroundTasksUrl)
       : translate('component_navigation.status.pending');
     metaList.push(
-      <li key="isPending" data-toggle="tooltip" title={tooltip}>
-        <PendingIcon /> <span>{translate('background_task.status.PENDING')}</span>
-      </li>
+      <Tooltip
+        key="isPending"
+        overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />}
+        mouseLeaveDelay={2}>
+        <li>
+          <PendingIcon /> <span>{translate('background_task.status.PENDING')}</span>
+        </li>
+      </Tooltip>
     );
   } else if (props.isFailed) {
     const tooltip = canSeeBackgroundTasks
       ? translateWithParameters('component_navigation.status.failed.admin', backgroundTasksUrl)
       : translate('component_navigation.status.failed');
     metaList.push(
-      <li key="isFailed" data-toggle="tooltip" title={tooltip}>
-        <span className="badge badge-danger">
-          {translate('background_task.status.FAILED')}
-        </span>
-      </li>
+      <Tooltip
+        key="isFailed"
+        overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />}
+        mouseLeaveDelay={2}>
+        <li>
+          <span className="badge badge-danger">
+            {translate('background_task.status.FAILED')}
+          </span>
+        </li>
+      </Tooltip>
     );
   }
-
   if (props.analysisDate) {
     metaList.push(
       <li key="analysisDate">
-        {moment(props.analysisDate).format('LLL')}
+        <DateTimeFormatter date={props.analysisDate} />
       </li>
     );
   }
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 (file)
index fd99e28..0000000
+++ /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(
-        <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} />
-      ).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 (file)
index 0000000..e5f0a6a
--- /dev/null
@@ -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(
+        <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} />
+      ).find('IncrementalBadge')
+    ).toHaveLength(incremental ? 1 : 0);
+  }
+});
index 7c458767be0a54cc7aaa29c722b146708d6d04f6..71635357dacf3e8545bddb7eacbd183589a2f677 100644 (file)
@@ -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;
index b81b2cf256fe46b4a7fa1418967d8acbca0419d7..cd5f6d39feab22eef71b85d97a646cdbbb7a16e4 100644 (file)
  * 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 (
     <div className="account-project-card clearfix">
       <aside className="account-project-side">
         {isAnalyzed
-          ? <div
-              className="account-project-analysis"
-              title={analysisMoment ? analysisMoment.format('LLL') : undefined}>
-              {translateWithParameters(
-                'my_account.projects.analyzed_x',
-                analysisMoment ? analysisMoment.fromNow() : undefined
-              )}
-            </div>
+          ? <Tooltip
+              overlay={<DateTimeFormatter date={project.lastAnalysisDate} />}
+              placement="right">
+              <div className="account-project-analysis">
+                <FormattedRelative value={project.lastAnalysisDate}>
+                  {(relativeDate: string) =>
+                    <span>
+                      {translateWithParameters('my_account.projects.analyzed_x', relativeDate)}
+                    </span>}
+                </FormattedRelative>
+              </div>
+            </Tooltip>
           : <div className="account-project-analysis">
               {translate('my_account.projects.never_analyzed')}
             </div>}
index f681f8af62fca7b22c3a5e6a2b3263c433739d7b..aa597b1ce8c536c4f9b31662374cc0016e63c273 100644 (file)
@@ -49,9 +49,7 @@ it('should not render optional fields', () => {
 it('should render analysis date', () => {
   const project = { ...BASE, lastAnalysisDate: '2016-05-17' };
   const output = shallow(<ProjectCard project={project} />);
-  expect(output.find('.account-project-analysis').text()).toContain(
-    'my_account.projects.analyzed_x'
-  );
+  expect(output.find('.account-project-analysis FormattedRelative')).toHaveLength(1);
 });
 
 it('should not render analysis date', () => {
index 98bd76ece05aac90ae41609879e528fcbc467ea2..8fe7a69b02820afadba40690f9a3315c347a8be5 100644 (file)
  * 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 $ from 'jquery';
-import moment from 'moment';
 import React, { Component } from 'react';
-import { DATE_FORMAT } from '../constants';
+import { toShortNotSoISOString, isValidDate } from '../../../helpers/dates';
 
 export default class DateFilter extends Component {
   componentDidMount() {
@@ -47,17 +45,15 @@ export default class DateFilter extends Component {
 
   handleChange() {
     const date = {};
-    const minDateRaw = this.refs.minDate.value;
-    const maxDateRaw = this.refs.maxDate.value;
-    const minDate = moment(minDateRaw, DATE_FORMAT, true);
-    const maxDate = moment(maxDateRaw, DATE_FORMAT, true);
+    const minDate = new Date(this.refs.minDate.value);
+    const maxDate = new Date(this.refs.maxDate.value);
 
-    if (minDate.isValid()) {
-      date.minSubmittedAt = minDate.format(DATE_FORMAT);
+    if (isValidDate(minDate)) {
+      date.minSubmittedAt = toShortNotSoISOString(minDate);
     }
 
-    if (maxDate.isValid()) {
-      date.maxExecutedAt = maxDate.format(DATE_FORMAT);
+    if (isValidDate(maxDate)) {
+      date.maxExecutedAt = toShortNotSoISOString(maxDate);
     }
 
     this.props.onChange(date);
index 7f49e254a416fa5c199c4e2c69866ebf3f436a28..3c93857dadbccc2ce44aea0e8585de4f3a26f636 100644 (file)
@@ -48,9 +48,9 @@ export default class Task extends React.PureComponent {
         <TaskComponent task={task} />
         <TaskId task={task} />
         <TaskDay task={task} prevTask={prevTask} />
-        <TaskDate date={task.submittedAt} baseDate={task.submittedAt} format="LTS" />
-        <TaskDate date={task.startedAt} baseDate={task.submittedAt} format="LTS" />
-        <TaskDate date={task.executedAt} baseDate={task.submittedAt} format="LTS" />
+        <TaskDate date={task.submittedAt} baseDate={task.submittedAt} />
+        <TaskDate date={task.startedAt} baseDate={task.submittedAt} />
+        <TaskDate date={task.executedAt} baseDate={task.submittedAt} />
         <TaskExecutionTime task={task} />
         <TaskActions
           component={component}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js
deleted file mode 100644 (file)
index 117702b..0000000
+++ /dev/null
@@ -1,44 +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 React from 'react';
-
-const TaskDate = (
-  { date, baseDate, format } /*: {
-  date: string,
-  baseDate: string,
-  format: string
-} */
-) => {
-  const m = moment(date);
-  const baseM = moment(baseDate);
-  const diff = date && baseDate ? m.diff(baseM, 'days') : 0;
-
-  return (
-    <td className="thin nowrap text-right">
-      {diff > 0 && <span className="text-warning little-spacer-right">{`(+${diff}d)`}</span>}
-
-      {date ? moment(date).format(format) : ''}
-    </td>
-  );
-};
-
-export default TaskDate;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx
new file mode 100644 (file)
index 0000000..d03a3e6
--- /dev/null
@@ -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 '../../../components/intl/TimeFormatter';
+import { differenceInDays, isValidDate } from '../../../helpers/dates';
+
+interface Props {
+  date: string;
+  baseDate: string;
+}
+
+export default function TaskDate({ date, baseDate }: Props) {
+  const parsedDate = new Date(date);
+  const parsedBaseDate = new Date(baseDate);
+  const diff =
+    date && baseDate && isValidDate(parsedDate) && isValidDate(parsedBaseDate)
+      ? differenceInDays(parsedDate, parsedBaseDate)
+      : 0;
+
+  return (
+    <td className="thin nowrap text-right">
+      {diff > 0 && <span className="text-warning little-spacer-right">{`(+${diff}d)`}</span>}
+
+      {date && isValidDate(parsedDate) ? <TimeFormatter date={parsedDate} long={true} /> : ''}
+    </td>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js
deleted file mode 100644 (file)
index 8307b34..0000000
+++ /dev/null
@@ -1,39 +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 React from 'react';
-/*:: import type { Task } from '../types'; */
-
-function isAnotherDay(a, b) {
-  return !moment(a).isSame(moment(b), 'day');
-}
-
-const TaskDay = ({ task, prevTask } /*: { task: Task, prevTask: ?Task } */) => {
-  const shouldDisplay = !prevTask || isAnotherDay(task.submittedAt, prevTask.submittedAt);
-
-  return (
-    <td className="thin nowrap text-right">
-      {shouldDisplay ? moment(task.submittedAt).format('LL') : ''}
-    </td>
-  );
-};
-
-export default TaskDay;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx
new file mode 100644 (file)
index 0000000..ed6a79c
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 '../../../components/intl/DateFormatter';
+import { isSameDay } from '../../../helpers/dates';
+import { ITask } from '../types';
+
+interface Props {
+  task: ITask;
+  prevTask?: ITask;
+}
+
+export default function TaskDay({ task, prevTask }: Props) {
+  const shouldDisplay =
+    !prevTask || !isSameDay(new Date(task.submittedAt), new Date(prevTask.submittedAt));
+
+  return (
+    <td className="thin nowrap text-right">
+      {shouldDisplay ? <DateFormatter date={task.submittedAt} long={true} /> : ''}
+    </td>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/types.js b/server/sonar-web/src/main/js/apps/background-tasks/types.js
deleted file mode 100644 (file)
index e272937..0000000
+++ /dev/null
@@ -1,25 +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.
- */
-/*::
-export type Task = {
-  incremental: boolean,
-  id: string
-};
-*/
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/types.ts b/server/sonar-web/src/main/js/apps/background-tasks/types.ts
new file mode 100644 (file)
index 0000000..c517f20
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+export interface ITask {
+  incremental: boolean;
+  id: string;
+  submittedAt: string;
+}
index ef72a1d213bbb9d0e1ff8320127666ae71c8fb9a..e8b41e80e364aefcdf63024500d9bdce0446f7ac 100644 (file)
@@ -20,7 +20,8 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
-import moment from 'moment';
+import { FormattedRelative } from 'react-intl';
+import DateFormatter from '../../../components/intl/DateFormatter';
 import Tooltip from '../../../components/controls/Tooltip';
 import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -53,8 +54,13 @@ export default function LeakPeriodLegend({ className, component, period } /*: Pr
   }
 
   const date = getPeriodDate(period);
-  const fromNow = moment(date).fromNow();
-  const tooltip = fromNow + ', ' + moment(date).format('LL');
+  const tooltip = (
+    <div>
+      <FormattedRelative value={date} />
+      {', '}
+      <DateFormatter date={date} long={true} />
+    </div>
+  );
   return (
     <Tooltip placement="left" overlay={tooltip}>
       {label}
index b4c91f405cc01d01f6c7509ec1831082d763f88d..cf03a171d55fc1ed7c5078848d3ee0fe9bf52a0f 100644 (file)
@@ -20,7 +20,6 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
-import moment from 'moment';
 import Breadcrumbs from './Breadcrumbs';
 import FilesView from '../drilldown/FilesView';
 import MeasureFavoriteContainer from './MeasureFavoriteContainer';
@@ -217,15 +216,13 @@ export default class MeasureContent extends React.PureComponent {
   renderCode() {
     const { component, leakPeriod } = this.props;
     const leakPeriodDate =
-      isDiffMetric(this.props.metric.key) && leakPeriod != null
-        ? moment(leakPeriod.date).toDate()
-        : null;
+      isDiffMetric(this.props.metric.key) && leakPeriod != null ? new Date(leakPeriod.date) : null;
 
     let filterLine;
     if (leakPeriodDate != null) {
       filterLine = line => {
         if (line.scmDate) {
-          const scmDate = moment(line.scmDate).toDate();
+          const scmDate = new Date(line.scmDate);
           return scmDate >= leakPeriodDate;
         } else {
           return false;
index 4f7fce011d7bb9e1cce66ab6938a269f572240c5..9becda572f1c2eafd2a7ce779661725ef9c992a5 100644 (file)
@@ -45,12 +45,6 @@ const PERIOD_DAYS = {
   parameter: '18'
 };
 
-jest.mock('moment', () => () => ({
-  format: () => 'March 1, 2017 9:36 AM',
-  fromNow: () => 'a month ago',
-  toDate: () => 'date'
-}));
-
 it('should render correctly', () => {
   expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD} />)).toMatchSnapshot();
   expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD_DAYS} />)).toMatchSnapshot();
index 0f0e328d85ec572dc16f2f1b4081395ebe828a58..b82411aaf929556c7b9dd0d7d6ca2f719c3d5fed 100644 (file)
@@ -2,7 +2,19 @@
 
 exports[`should render correctly 1`] = `
 <Tooltip
-  overlay="a month ago, March 1, 2017 9:36 AM"
+  overlay={
+    <div>
+      <FormattedRelative
+        updateInterval={10000}
+        value={2017-05-16T11:50:02.000Z}
+      />
+      , 
+      <DateFormatter
+        date={2017-05-16T11:50:02.000Z}
+        long={true}
+      />
+    </div>
+  }
   placement="left"
 >
   <div
index 3a30ccd1ec486411f83b7b6186a3e0a65df2623c..8a409a34ac8fa31b5084f15cb7dc27a11567820b 100644 (file)
  */
 // @flow
 import React from 'react';
-import moment from 'moment';
 import { max } from 'lodash';
+import { FormattedRelative, intlShape } from 'react-intl';
+import { formatterOption, longFormatterOption } from '../../../components/intl/DateFormatter';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import FacetBox from '../../../components/facet/FacetBox';
 import FacetHeader from '../../../components/facet/FacetHeader';
 import FacetItem from '../../../components/facet/FacetItem';
 import { BarChart } from '../../../components/charts/bar-chart';
 import DateInput from '../../../components/controls/DateInput';
+import { isSameDay, toShortNotSoISOString } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
 /*:: import type { Component } from '../utils'; */
@@ -46,8 +49,6 @@ type Props = {|
 |};
 */
 
-const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ';
-
 export default class CreationDateFacet extends React.PureComponent {
   /*:: props: Props; */
 
@@ -55,6 +56,10 @@ export default class CreationDateFacet extends React.PureComponent {
     open: true
   };
 
+  static contextTypes = {
+    intl: intlShape
+  };
+
   property = 'createdAt';
 
   hasValue = () =>
@@ -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') + '<br>' + startMoment.format('LL');
-
-      if (endMoment) {
-        const isSameDay = endMoment.diff(startMoment, 'days') <= 1;
-        if (!isSameDay) {
-          tooltip += ' – ' + endMoment.format('LL');
-        }
+        formatMeasure(stats[start], 'SHORT_INT') +
+        '<br/>' +
+        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 (
       <div className="search-navigator-facet-container">
-        {m.format('LLL')}
+        <DateTimeFormatter date={this.props.createdAt} />
         <br />
         <span className="note">
-          ({m.fromNow()})
+          <FormattedRelative value={this.props.createdAt} />
         </span>
       </div>
     );
@@ -191,26 +194,26 @@ export default class CreationDateFacet extends React.PureComponent {
 
   renderPeriodSelectors() {
     const { createdAfter, createdBefore } = this.props;
-
+    const { formatDate } = this.context.intl;
     return (
       <div className="search-navigator-date-facet-selection">
         <DateInput
           className="search-navigator-date-facet-selection-dropdown-left"
-          onChange={this.handlePeriodChange('createdAfter')}
+          onChange={this.handlePeriodChangeAfter}
           placeholder={translate('from')}
-          value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined}
+          value={createdAfter ? formatDate(createdAfter, formatterOption) : undefined}
         />
         <DateInput
           className="search-navigator-date-facet-selection-dropdown-right"
-          onChange={this.handlePeriodChange('createdBefore')}
+          onChange={this.handlePeriodChangeBefore}
           placeholder={translate('to')}
-          value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined}
+          value={createdBefore ? formatDate(createdBefore, formatterOption) : undefined}
         />
       </div>
     );
   }
 
-  renderPrefefinedPeriods() {
+  renderPredefinedPeriods() {
     const { component, createdInLast, sinceLeakPeriod } = this.props;
     return (
       <div className="spacer-top issues-predefined-periods">
@@ -259,7 +262,7 @@ export default class CreationDateFacet extends React.PureComponent {
       : <div>
           {this.renderBarChart()}
           {this.renderPeriodSelectors()}
-          {this.renderPrefefinedPeriods()}
+          {this.renderPredefinedPeriods()}
         </div>;
   }
 
index 3c9785aa8be75e275dce748d5b38accfb12a98c6..aaca5381cb4bfe7cc183a9c41856be7e8c56c3b6 100644 (file)
@@ -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 {
       ? <ul className="text-left">
           {this.state.leaks.map(leak =>
             <li key={leak.project}>
-              {leak.projectName}: <FormattedDate date={leak.date} format="LL" />
+              {leak.projectName}: <DateTooltipFormatter date={leak.date} />
             </li>
           )}
         </ul>
index 88d26edef845df98c0363b019e07743a6cf9edbc..f932aeb8c8e6e1346f4b3dccd92014589668d051 100644 (file)
@@ -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 = (
+    <DateFormatter date={leakPeriodDate} long={true}>
+      {formattedLeakPeriodDate =>
+        <span>
+          {translateWithParameters(
+            ['date'].includes(period.mode)
+              ? 'overview.last_analysis_on_x'
+              : 'overview.started_on_x',
+            formattedLeakPeriodDate
+          )}
+        </span>}
+    </DateFormatter>
+  );
   return (
     <Tooltip overlay={tooltip} placement="top">
       <div className="overview-legend">
         {translateWithParameters('overview.leak_period_x', leakPeriodLabel)}
         <br />
-        <span className="note">
-          {note}
-        </span>
+        <FormattedRelative value={leakPeriodDate}>
+          {fromNow =>
+            <span className="note">
+              {translateWithParameters(
+                ['date'].includes(period.mode) ? 'overview.last_analysis_x' : 'overview.started_x',
+                fromNow
+              )}
+            </span>}
+        </FormattedRelative>
       </div>
     </Tooltip>
   );
index 11d0988237552be06a7b2a39a01468135175de8b..800a62d4ccd0a4a122dcbcd494e4ec6ee30799b7 100644 (file)
@@ -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;
index 251e20594bc72d4c03429b07543a27214ae134e2..b0179494ae03e41b34790a88f1f246e69b0245a9 100644 (file)
@@ -37,7 +37,9 @@ describe('check note', () => {
       mode: 'date',
       parameter: '2013-01-01'
     };
-    expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+    expect(
+      shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')
+    ).toMatchSnapshot();
   });
 
   it('version', () => {
@@ -46,7 +48,9 @@ describe('check note', () => {
       mode: 'version',
       parameter: '0.1'
     };
-    expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+    expect(
+      shallow(<LeakPeriodLegend period={period} />).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(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+    expect(shallow(<LeakPeriodLegend period={period} />).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(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+    expect(shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')).toHaveLength(1);
   });
 });
index 217405a65655fbaffedf27a835afd363af715056..cb842597822d143fbad8714c359a16cef0ea630b 100644 (file)
@@ -28,17 +28,15 @@ exports[`renders 2`] = `
       <li>
         Foo
         : 
-        <FormattedDate
+        <DateTooltipFormatter
           date="2017-01-01T11:39:03+0100"
-          format="LL"
         />
       </li>
       <li>
         Bar
         : 
-        <FormattedDate
+        <DateTooltipFormatter
           date="2017-02-01T11:39:03+0100"
-          format="LL"
         />
       </li>
     </ul>
index 397016097b3ae5bc7ae2d7df9fc92cbd393bd76a..44caddb1324595c5ccda0bde76cff11da09b58a5 100644 (file)
@@ -9,33 +9,15 @@ exports[`check note 10 days 1`] = `
 `;
 
 exports[`check note date 1`] = `
-<span
-  className="note"
->
-  overview.last_analysis_x.4 years ago
-</span>
-`;
-
-exports[`check note previous_analysis 1`] = `
-<span
-  className="note"
->
-  overview.started_x.4 years ago
-</span>
-`;
-
-exports[`check note previous_version 1`] = `
-<span
-  className="note"
->
-  overview.started_x.4 years ago
-</span>
+<FormattedRelative
+  updateInterval={10000}
+  value={2013-09-21T22:00:00.000Z}
+/>
 `;
 
 exports[`check note version 1`] = `
-<span
-  className="note"
->
-  overview.started_x.4 years ago
-</span>
+<FormattedRelative
+  updateInterval={10000}
+  value={2013-09-21T22:00:00.000Z}
+/>
 `;
index ef1b6d37e8d7744cb6e2e6e60a60f657c96b428e..5a9f7cc78071fcda4c6b1041489641e56a1f9bc1 100644 (file)
@@ -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 */) {
     <li className="overview-analysis">
       <div className="small little-spacer-bottom">
         <strong>
-          <FormattedDate date={analysis.date} format="LL" />
+          <DateTooltipFormatter date={analysis.date} placement="right" />
         </strong>
       </div>
 
index be23bc7bc070d3252a16c19de0a76f846d6e059c..bf62a5bd1cc5eecd926e96f41244049e9b68bd7f 100644 (file)
@@ -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 (
-    <div className="overview-analysis-event">
+    <span className="overview-analysis-event">
       <span className="note">{translate('event.category', event.category)}:</span>{' '}
-      <Tooltip overlay={event.description} placement="left">
-        <strong>
-          {event.name}
-        </strong>
-      </Tooltip>
-    </div>
+      {event.description
+        ? <Tooltip overlay={event.description} placement="left" mouseEnterDelay={0.5}>
+            <strong>
+              {event.name}
+            </strong>
+          </Tooltip>
+        : <strong>
+            {event.name}
+          </strong>}
+    </span>
   );
 }
index d4bc3609a6d8cb1fcb4846389f97d1e5e15da911..c1a898a3bf1d7d54f6411500fd48d91f6b6661c6 100644 (file)
@@ -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 {
       <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}>
         <div className="overview-analysis-graph-tooltip">
           <div className="overview-analysis-graph-tooltip-title">
-            <FormattedDate date={this.props.selectedDate} format="LL" />
+            <DateFormatter date={this.props.selectedDate} long={true} />
           </div>
           <table className="width-100">
             <tbody>
index e37d21f47d99699e98ec50e578a0b402b424f5ae..33bc4e28c18612060bc5447a9cee0c00eb83152b 100644 (file)
@@ -8,9 +8,9 @@ exports[`should sort the events with version first 1`] = `
     className="small little-spacer-bottom"
   >
     <strong>
-      <FormattedDate
+      <DateTooltipFormatter
         date="2017-06-10T16:10:59+0200"
-        format="LL"
+        placement="right"
       />
     </strong>
   </div>
index 18c6f76ec731036cdbac4167b23ad3d87fe053f0..802c927a96ce80d52d73abb29e8d56516bbdec6e 100644 (file)
@@ -9,7 +9,7 @@ exports[`should render a version correctly 1`] = `
 `;
 
 exports[`should render an event correctly 1`] = `
-<div
+<span
   className="overview-analysis-event"
 >
   <span
@@ -19,12 +19,8 @@ exports[`should render an event correctly 1`] = `
     :
   </span>
    
-  <Tooltip
-    placement="left"
-  >
-    <strong>
-      test
-    </strong>
-  </Tooltip>
-</div>
+  <strong>
+    test
+  </strong>
+</span>
 `;
index 0d0aec9012c0619c9d0157285774bf974b3dc1c6..64d9d39a3e4aac84f856a9d3262127c084eed8bf 100644 (file)
@@ -17,9 +17,9 @@ exports[`should render correctly 1`] = `
     <div
       className="overview-analysis-graph-tooltip-title"
     >
-      <FormattedDate
+      <DateFormatter
         date={2011-10-25T10:27:41.000Z}
-        format="LL"
+        long={true}
       />
     </div>
     <table
index 00e81e9d47ac5104ef61741acf2a25dae0bc14fa..2eab616e1957113316a7d3eb17fb4a888fbc7c4f 100644 (file)
  * 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 React from 'react';
 import { Link } from 'react-router';
+import { FormattedRelative } from 'react-intl';
 import Tooltip from '../../../components/controls/Tooltip';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import enhance from './enhance';
 import { getMetricName } from '../helpers/metrics';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -43,9 +44,14 @@ class CodeSmells extends React.PureComponent {
       Object.assign(params, { sinceLeakPeriod: 'true' });
     }
 
-    const formattedAnalysisDate = moment(component.analysisDate).format('LLL');
-    const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate);
-
+    const tooltip = (
+      <DateTimeFormatter date={component.analysisDate}>
+        {formattedAnalysisDate =>
+          <span>
+            {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)}
+          </span>}
+      </DateTimeFormatter>
+    );
     return (
       <Tooltip overlay={tooltip} placement="top">
         <Link to={getComponentIssuesUrl(component.key, params)}>
@@ -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 (
-      <span className="overview-domain-timeline-date">
-        {translateWithParameters('overview.started_x', fromNow)}
-      </span>
+      <FormattedRelative value={this.props.historyStartDate}>
+        {fromNow =>
+          <span className="overview-domain-timeline-date">
+            {translateWithParameters('overview.started_x', fromNow)}
+          </span>}
+      </FormattedRelative>
     );
   }
 
index b4be68adcb1f212104b6e28421a2a69420f3411b..fb008cabb12be3d095aa9c5d12e094e7ab2035c7 100644 (file)
@@ -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 = (
+        <DateTimeFormatter date={component.analysisDate}>
+          {formattedAnalysisDate =>
+            <span>
+              {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)}
+            </span>}
+        </DateTimeFormatter>
+      );
+
       return (
         <Tooltip overlay={tooltip} placement="top">
           <Link to={getComponentIssuesUrl(component.key, params)}>
index 06b8994c23d7900103a1d95a7384843d119c30ba..1bc54255ce1df90e4ae8ca53824242f58abb077d 100644 (file)
@@ -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",
         },
index 2ee3b2cfe3daefa76c3ffc5a752c4a8ba9f2d9c4..455c679d2d7c404969e87551b45d076048c80602 100644 (file)
  */
 // @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();
index 4e5be7ea5db69f9b4b991f31540afc15e667a33f..044b1462323b1af002dd90cb76d7ded559e07d42 100644 (file)
@@ -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');
index 4eb0e719a28163ac6d172b597439057df1fd110f..320f934e028b2ee5e46c28ff6be72716a3953656 100644 (file)
@@ -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 {
       <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}>
         <div className="project-activity-graph-tooltip">
           <div className="project-activity-graph-tooltip-title spacer-bottom">
-            <FormattedDate date={this.props.selectedDate} format="LL" />
+            <DateTimeFormatter date={this.props.selectedDate} />
           </div>
           <table className="width-100">
             <tbody>
index 7f69288c33d974350fd7953343144dc41dec4484..7b94b01483c58cb21a903175857ea818a928c0cb 100644 (file)
 // @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 {
                 </div>}
               <ul className="project-activity-days-list">
                 {days.map(day =>
-                  <li
-                    key={day}
-                    className="project-activity-day"
-                    data-day={moment(Number(day)).format('YYYY-MM-DD')}>
+                  <li key={day} className="project-activity-day">
                     <div className="project-activity-date">
-                      <FormattedDate date={Number(day)} format="LL" />
+                      <DateFormatter date={Number(day)} long={true} />
                     </div>
                     <ul className="project-activity-analyses-list">
                       {version.byDay[day] != null &&
index f52a59b594318669631d66862f10579372fbc8b9..bc17b6a46b68e2e46a3f84dd23a2163461ba6f45 100644 (file)
@@ -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">
         <div className="project-activity-time spacer-right">
-          <FormattedDate className="text-middle" date={date} format="LT" tooltipFormat="LTS" />
+          <TimeTooltipFormatter className="text-middle" date={date} placement="right" />
         </div>
         <div className="project-activity-analysis-icon big-spacer-right" title={analysisTitle} />
 
index cdc41b48e997826f460930a8377fe068d24a92cd..2de698d4b5281c7c030f74103d41fd189c12f01b 100644 (file)
@@ -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 */) {
         <div className="project-activity-layout-page-main">
           <ProjectActivityGraphs
             analyses={analyses}
-            leakPeriodDate={moment(props.project.leakPeriodDate).toDate()}
+            leakPeriodDate={new Date(props.project.leakPeriodDate)}
             loading={props.graphLoading}
             measuresHistory={measuresHistory}
             metrics={props.metrics}
index 61e9bd077c2729be243ef3a995426affcf7b2aaf..172e0d35bcf75665b4522686a862359e25805d19 100644 (file)
@@ -19,7 +19,6 @@
  */
 // @flow
 import React from 'react';
-import moment from 'moment';
 import { connect } from 'react-redux';
 import { withRouter } from 'react-router';
 import ProjectActivityApp from './ProjectActivityApp';
@@ -173,7 +172,7 @@ class ProjectActivityAppContainer extends React.PureComponent {
     return api
       .getProjectActivity({ ...parameters, ...additional })
       .then(({ analyses, paging }) => ({
-        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
           }))
         })),
index 1ece1355970c8257220eae52a47b52a00beb36e2..f6f85c29c9b180a29bdbb15c9fd4e4d78daa989d 100644 (file)
@@ -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 (
index 30864a995f99b53bd22c3700bcc48b3948af7012..6a0c61741f2cd7949b501753b10871f912a2f9f5 100644 (file)
@@ -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(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />)).toMatchSnapshot();
 });
index fa19966456151300ed23693ca9c6e70cbe32c2a8..6b93eaae2966e9331a2a14a5e295e809d097954d 100644 (file)
  * 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(
       <ProjectActivityDateInput
         from={new Date('2016-10-27T12:21:15+0000')}
         to={new Date('2016-12-27T12:21:15+0000')}
index aea593d413e4d92f19471eb266882d78a0bc6076..e2ab4b039a81d6320803d34fb6fd51fc582f874a 100644 (file)
@@ -17,9 +17,8 @@ exports[`should not add separators if not needed 1`] = `
     <div
       className="project-activity-graph-tooltip-title spacer-bottom"
     >
-      <FormattedDate
+      <DateTimeFormatter
         date={2011-10-01T22:01:00.000Z}
-        format="LL"
       />
     </div>
     <table
@@ -53,9 +52,8 @@ exports[`should render correctly for issues graphs 1`] = `
     <div
       className="project-activity-graph-tooltip-title spacer-bottom"
     >
-      <FormattedDate
+      <DateTimeFormatter
         date={2011-10-01T22:01:00.000Z}
-        format="LL"
       />
     </div>
     <table
@@ -109,9 +107,8 @@ exports[`should render correctly for random graphs 1`] = `
     <div
       className="project-activity-graph-tooltip-title spacer-bottom"
     >
-      <FormattedDate
+      <DateTimeFormatter
         date={2011-10-25T10:27:41.000Z}
-        format="LL"
       />
     </div>
     <table
index 326220854443245e14f5a1e90cdcfea9d3c8ad6a..00938d611a838d1b4acef20a69db9d5e0b6b5138 100644 (file)
@@ -25,14 +25,13 @@ exports[`should correctly filter analyses by category 1`] = `
     >
       <li
         className="project-activity-day"
-        data-day="Formated.YYYY-MM-DD:2016-9-24"
       >
         <div
           className="project-activity-date"
         >
-          <FormattedDate
-            date="2016-9-24"
-            format="LL"
+          <DateFormatter
+            date="1477267200000"
+            long={true}
           />
         </div>
         <ul
@@ -95,14 +94,13 @@ exports[`should correctly filter analyses by date range 1`] = `
     >
       <li
         className="project-activity-day"
-        data-day="Formated.YYYY-MM-DD:2016-9-27"
       >
         <div
           className="project-activity-date"
         >
-          <FormattedDate
-            date="2016-9-27"
-            format="LL"
+          <DateFormatter
+            date="1477526400000"
+            long={true}
           />
         </div>
         <ul
@@ -165,14 +163,13 @@ exports[`should render correctly 1`] = `
     >
       <li
         className="project-activity-day"
-        data-day="Formated.YYYY-MM-DD:2016-9-27"
       >
         <div
           className="project-activity-date"
         >
-          <FormattedDate
-            date="2016-9-27"
-            format="LL"
+          <DateFormatter
+            date="1477526400000"
+            long={true}
           />
         </div>
         <ul
@@ -241,14 +238,13 @@ exports[`should render correctly 1`] = `
     >
       <li
         className="project-activity-day"
-        data-day="Formated.YYYY-MM-DD:2016-9-26"
       >
         <div
           className="project-activity-date"
         >
-          <FormattedDate
-            date="2016-9-26"
-            format="LL"
+          <DateFormatter
+            date="1477440000000"
+            long={true}
           />
         </div>
         <ul
@@ -288,14 +284,13 @@ exports[`should render correctly 1`] = `
       </li>
       <li
         className="project-activity-day"
-        data-day="Formated.YYYY-MM-DD:2016-9-24"
       >
         <div
           className="project-activity-date"
         >
-          <FormattedDate
-            date="2016-9-24"
-            format="LL"
+          <DateFormatter
+            date="1477267200000"
+            long={true}
           />
         </div>
         <ul
index bc15b05c60e54b331acd5304b57248758fdc7357..95d05668feff5f8564be36bb7c9c3afa9c705389 100644 (file)
@@ -8,7 +8,7 @@ exports[`should render correctly the date inputs 1`] = `
     name="from"
     onChange={[Function]}
     placeholder="from"
-    value="2016-10-27"
+    value="10/27/2016"
   />
   —
   <DateInput
@@ -17,7 +17,7 @@ exports[`should render correctly the date inputs 1`] = `
     name="to"
     onChange={[Function]}
     placeholder="to"
-    value="2016-12-27"
+    value="12/27/2016"
   />
   <button
     className="spacer-left"
index 12694dba2bf147e41b799679f448bebc4d434fde..03eb0510b61d1d9cd75886d69e4b966be069dc45 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 // @flow
-import moment from 'moment';
 import { chunk, flatMap, groupBy, isEqual, sortBy } from 'lodash';
 import {
   cleanQuery,
@@ -29,6 +28,7 @@ import {
   serializeDate,
   serializeString
 } from '../../helpers/query';
+import { startOfDay } from '../../helpers/dates';
 import { getLocalizedMetricName, translate } from '../../helpers/l10n';
 /*:: import type { Analysis, MeasureHistory, Metric, Query } from './types'; */
 /*:: import type { RawQuery } from '../../helpers/query'; */
@@ -157,7 +157,7 @@ export function getAnalysesByVersionByDay(analyses /*: Array<Analysis> */, query
       acc.push(currentVersion);
     }
 
-    const day = moment(analysis.date).startOf('day').valueOf().toString();
+    const day = startOfDay(new Date(analysis.date)).getTime().toString();
 
     let matchFilters = true;
     if (query.category || query.from || query.to) {
index b6af4d5cd10b4b952491103b01da2e3051105f34..695136bb6c679edc5884dfdb87aed76d0b117387 100644 (file)
@@ -20,8 +20,9 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
-import moment from 'moment';
 import { Link } from 'react-router';
+import { FormattedRelative } from 'react-intl';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter.tsx';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
 import ProjectCardLeakMeasures from './ProjectCardLeakMeasures';
 import FavoriteContainer from '../../../components/controls/FavoriteContainer';
@@ -90,19 +91,19 @@ export default function ProjectCardLeak({ measures, organization, project } /*:
           hasLeakPeriodStart &&
           <div className="project-card-dates note text-right pull-right">
             {hasLeakPeriodStart &&
-              <span className="project-card-leak-date pull-right">
-                {translateWithParameters(
-                  'projects.leak_period_x',
-                  moment(project.leakPeriodDate).fromNow()
-                )}
-              </span>}
+              <FormattedRelative value={project.leakPeriodDate}>
+                {fromNow =>
+                  <span className="project-card-leak-date pull-right">
+                    {translateWithParameters('projects.leak_period_x', fromNow)}
+                  </span>}
+              </FormattedRelative>}
             {isProjectAnalyzed &&
-              <span>
-                {translateWithParameters(
-                  'projects.last_analysis_on_x',
-                  moment(project.analysisDate).format('LLL')
-                )}
-              </span>}
+              <DateTimeFormatter date={project.analysisDate}>
+                {formattedDate =>
+                  <span>
+                    {translateWithParameters('projects.last_analysis_on_x', formattedDate)}
+                  </span>}
+              </DateTimeFormatter>}
           </div>}
       </div>
 
index 5d8cddae025dd3ef7022833eac00ba28921ebcd0..baea9ad442e8e7c88ae39715aa5dfaddcc8eb898 100644 (file)
@@ -20,8 +20,8 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
-import moment from 'moment';
 import { Link } from 'react-router';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
 import ProjectCardOverallMeasures from './ProjectCardOverallMeasures';
 import FavoriteContainer from '../../../components/controls/FavoriteContainer';
@@ -87,12 +87,12 @@ export default function ProjectCardOverall({ measures, organization, project } /
         </div>
         {isProjectAnalyzed &&
           <div className="project-card-dates note text-right">
-            <span className="big-spacer-left">
-              {translateWithParameters(
-                'projects.last_analysis_on_x',
-                moment(project.analysisDate).format('LLL')
-              )}
-            </span>
+            <DateTimeFormatter date={project.analysisDate}>
+              {formattedDate =>
+                <span className="big-spacer-left">
+                  {translateWithParameters('projects.last_analysis_on_x', formattedDate)}
+                </span>}
+            </DateTimeFormatter>
           </div>}
       </div>
 
index 46819f2bd13bd68bf61675d507f485507dbe026d..2ac25e044ab094ca6848d304867e57fb9feb50a1 100644 (file)
@@ -35,15 +35,11 @@ const MEASURES = {
   new_bugs: 12
 };
 
-jest.mock('moment', () => () => ({
-  format: () => 'March 1, 2017 9:36 AM',
-  fromNow: () => 'a month ago'
-}));
-
 it('should display analysis date and leak start date', () => {
   const card = shallow(<ProjectCardLeak type="leak" measures={MEASURES} project={PROJECT} />);
   expect(card.find('.project-card-dates').exists()).toBeTruthy();
-  expect(card.find('.project-card-dates').find('span').getNodes()).toHaveLength(2);
+  expect(card.find('.project-card-dates').find('FormattedRelative').getNodes()).toHaveLength(1);
+  expect(card.find('.project-card-dates').find('DateTimeFormatter').getNodes()).toHaveLength(1);
 });
 
 it('should not display analysis date or leak start date', () => {
index b6f5698f23161d62e590298edd6ac8520f686672..91144662ec59d37d2b675e59d97116cdafc5396f 100644 (file)
@@ -35,11 +35,6 @@ const MEASURES = {
   new_bugs: 12
 };
 
-jest.mock('moment', () => () => ({
-  format: () => 'March 1, 2017 9:36 AM',
-  fromNow: () => 'a month ago'
-}));
-
 it('should display analysis date (and not leak period) when defined', () => {
   expect(
     shallow(<ProjectCardOverall measures={{}} project={PROJECT} />)
index cb539fbafe65bead801505abb149af1391b4e4de..591ed49e1c8f97b595c2cc8d33b9fb909594b823 100644 (file)
@@ -35,14 +35,13 @@ exports[`should display the leak measures and quality gate 1`] = `
     <div
       className="project-card-dates note text-right pull-right"
     >
-      <span
-        className="project-card-leak-date pull-right"
-      >
-        projects.leak_period_x.a month ago
-      </span>
-      <span>
-        projects.last_analysis_on_x.March 1, 2017 9:36 AM
-      </span>
+      <FormattedRelative
+        updateInterval={10000}
+        value="2016-12-01"
+      />
+      <DateTimeFormatter
+        date="2017-01-01"
+      />
     </div>
   </div>
   <div
index b93bf252c3145083d3575d96a8370a9c2e12ed2b..720f0f2012930fe7fa5d819c2b1b4dddaf4d0ffc 100644 (file)
@@ -35,11 +35,9 @@ exports[`should display the overall measures and quality gate 1`] = `
     <div
       className="project-card-dates note text-right"
     >
-      <span
-        className="big-spacer-left"
-      >
-        projects.last_analysis_on_x.March 1, 2017 9:36 AM
-      </span>
+      <DateTimeFormatter
+        date="2017-01-01"
+      />
     </div>
   </div>
   <div
index 1dbfdda95fd40d7a82d48d9801c4bb0711563ef8..1e94f724259824804ca8827dc96f84555f4c3a2e 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
-import * as moment from 'moment';
 import ChangesList from './ChangesList';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { translate } from '../../../helpers/l10n';
 import { getRulesUrl } from '../../../helpers/urls';
+import { differenceInSeconds } from '../../../helpers/dates';
 import { ProfileChangelogEvent } from '../types';
 
 interface Props {
@@ -35,7 +36,8 @@ export default function Changelog(props: Props) {
 
   const rows = props.events.map((event, index) => {
     const prev = index > 0 ? props.events[index - 1] : null;
-    const isSameDate = prev != null && moment(prev.date).diff(event.date, 'seconds') < 10;
+    const isSameDate =
+      prev != null && differenceInSeconds(new Date(prev.date), new Date(event.date)) < 10;
     const isBulkChange =
       prev != null &&
       isSameDate &&
@@ -51,7 +53,7 @@ export default function Changelog(props: Props) {
     return (
       <tr key={index} className={className}>
         <td className="thin nowrap">
-          {!isBulkChange && moment(event.date).format('LLL')}
+          {!isBulkChange && <DateTimeFormatter date={event.date} />}
         </td>
 
         <td className="thin nowrap">
index b28f8e236e006e5324e2cb6af05d265deedb7f34..6272bcc2f12683e62b6ccd3cf349795e7c7fb668 100644 (file)
@@ -45,7 +45,7 @@ it('should render events', () => {
 it('should render event date', () => {
   const events = [createEvent()];
   const changelog = shallow(<Changelog events={events} organization={null} />);
-  expect(changelog.text()).toContain('2016');
+  expect(changelog.find('DateTimeFormatter')).toHaveLength(1);
 });
 
 it('should render author', () => {
index 947b15e419cc697fb9a5e2577e24a38569a643e6..3841c70ee0fa4c874b9c99c6ba1ec60e011417ff 100644 (file)
@@ -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
-    ? <span title={moment(date).format('LLL')} data-toggle="tooltip">
-        {moment(date).fromNow()}
-      </span>
+    ? <Tooltip overlay={<DateTimeFormatter date={date} />}>
+        <span>
+          <FormattedRelative value={date} />
+        </span>
+      </Tooltip>
     : <span>
         {translate('never')}
       </span>;
index 84df54ceb49769743e473ad6039f005bd20912c9..2119c23ae3e05b610f708da97b1aa385f0d8f36a 100644 (file)
  */
 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<Props, State> {
+  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<Props, State> {
 
   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<Props, State> {
     }
 
     const newRulesUrl = getRulesUrl(
-      {
-        available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD')
-      },
+      { available_since: this.periodStartDate },
       this.props.organization
     );
 
index 578020e71297eb88361b6f8fd4a42f1ef9b0f3c0..5252d1c184004235c9a4331223c4a684ba1c4bbe 100644 (file)
@@ -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}
               </ProfileLink>
             </div>
-            <div className="note">
-              {profile.languageName}
-              {', '}
-              updated on {moment(profile.rulesUpdatedAt).format('LL')}
-            </div>
+            <DateFormatter date={profile.rulesUpdatedAt} long={true}>
+              {formattedDate =>
+                <div className="note">
+                  {translateWithParameters(
+                    'quality_profiles.x_updated_on_y',
+                    profile.languageName,
+                    formattedDate
+                  )}
+                </div>}
+            </DateFormatter>
           </li>
         )}
       </ul>
index cc7140c0fd49b5855b8e70bfeae4a89f431b8098..d058aa01acdc1dc30d556da706e6af7312c1bb59 100644 (file)
@@ -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) =>
index f8ef6d4c7f09490ae0d8e94c4804d38e0cda161f..d6b26fd3a7570ee4ee098c222d4647129a13f320 100644 (file)
@@ -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 {
         <td className="js-expiration text-middle">
           {license.expiration != null &&
             <div className={license.invalidExpiration ? 'text-danger' : null}>
-              {moment(license.expiration).format('LL')}
+              <DateFormatter date={license.expiration} long={true} />
             </div>}
         </td>
         <td className="js-type text-middle">
index 4d7c758d416e801f99b24bc7e1feefdc6761cc7e..94e3d55b633c3ca834e4fd72d7a7d234b816f819 100644 (file)
@@ -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 (file)
index 0896127..0000000
+++ /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 (
-        <g key={tick}>
-          <text
-            className="line-chart-tick line-chart-tick-x"
-            dx="-1em"
-            dy="0.3em"
-            textAnchor="end"
-            {...opts}>
-            {this.props.formatYTick(tick)}
-          </text>
-          <line
-            className="line-chart-grid"
-            x1={xScale.range()[0]}
-            x2={xScale.range()[1]}
-            y1={yScale(tick)}
-            y2={yScale(tick)}
-          />
-        </g>
-      );
-    });
-
-    return (
-      <g>
-        {grid}
-      </g>
-    );
-  },
-
-  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 (
-        <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">
-          {format(tick)}
-        </text>
-      );
-    });
-
-    return (
-      <g>
-        {ticks}
-      </g>
-    );
-  },
-
-  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 <rect {...opts} />;
-  },
-
-  renderLine(xScale, yScale) {
-    const p = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y));
-    if (this.props.basisCurve) {
-      p.curve(curveBasis);
-    }
-    return <path className="line-chart-path" d={p(this.props.data)} />;
-  },
-
-  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 = [
-          `<span class="nowrap">${event.version}</span>`,
-          `<span class="nowrap">${moment(event.date).format('LL')}</span>`,
-          `<span class="nowrap">${value}</span>`
-        ].join('<br>');
-        return (
-          <circle
-            key={key}
-            className={className}
-            r="4"
-            cx={xScale(event.snapshot.x)}
-            cy={yScale(event.snapshot.y)}
-            onMouseEnter={this.handleEventMouseEnter.bind(this, event)}
-            onMouseLeave={this.handleEventMouseLeave.bind(this, event)}
-            data-toggle="tooltip"
-            data-title={tooltip}
-          />
-        );
-      });
-    return (
-      <g>
-        {points}
-      </g>
-    );
-  },
-
-  render() {
-    if (!this.state.width || !this.state.height) {
-      return <div />;
-    }
-    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 (
-      <svg className="line-chart" width={this.state.width} height={this.state.height}>
-        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
-          {this.renderLeak(xScale, yScale)}
-          {this.renderHorizontalGrid(xScale, yScale)}
-          {this.renderTicks(xScale, yScale)}
-          {this.renderLine(xScale, yScale)}
-          {this.renderEvents(xScale, yScale)}
-        </g>
-      </svg>
-    );
-  }
-});
-export default Timeline;
index a1f4a4e70d9b3d1d22a4af0d5a7a4c471d2aa120..33f6c7d9cf85701387d30ad333d8876fb2076055 100644 (file)
@@ -45,7 +45,7 @@ export default class DateInput extends React.PureComponent<Props> {
   }
 
   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<Props> {
       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 (file)
index 0000000..0c78948
--- /dev/null
@@ -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 (
+    <FormattedDate
+      children={children}
+      value={date}
+      {...(long ? longFormatterOption : formatterOption)}
+    />
+  );
+}
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 (file)
index 0000000..560270c
--- /dev/null
@@ -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 <FormattedDate children={children} value={date} {...formatterOption} />;
+}
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 (file)
index 0000000..f8c7e90
--- /dev/null
@@ -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 (
+    <DateFormatter date={date} long={true}>
+      {formattedDate =>
+        <Tooltip
+          overlay={<DateTimeFormatter date={date} />}
+          placement={placement}
+          mouseEnterDelay={0.5}>
+          <time className={className} dateTime={new Date(date as Date).toISOString()}>
+            {formattedDate}
+          </time>
+        </Tooltip>}
+    </DateFormatter>
+  );
+}
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 (file)
index 0000000..9823aa8
--- /dev/null
@@ -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 (
+    <FormattedTime
+      children={children}
+      value={date}
+      {...(long ? longFormatterOption : formatterOption)}
+    />
+  );
+}
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 (file)
index 0000000..4ee3edc
--- /dev/null
@@ -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 (
+    <TimeFormatter date={date} long={false}>
+      {formattedTime =>
+        <Tooltip
+          overlay={<TimeFormatter date={date} long={true} />}
+          placement={placement}
+          mouseEnterDelay={0.5}>
+          <time className={className} dateTime={new Date(date as Date).toISOString()}>
+            {formattedTime}
+          </time>
+        </Tooltip>}
+    </TimeFormatter>
+  );
+}
index 721c83ff7e479828469ea4232dc4b64bdc3a45b4..404e395479ae0be8c3518fc1f341df03e8a7e6d6 100644 (file)
  */
 // @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 (
       <BubblePopupHelper
         isOpen={this.props.isOpen}
         position="bottomright"
         togglePopup={this.toggleChangelog}
         popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}>
-        <button
-          className="button-link issue-action issue-action-with-options js-issue-show-changelog"
-          title={momentCreationDate.format('LLL')}
-          onClick={this.handleClick}>
-          <span className="issue-meta-label">
-            {momentCreationDate.fromNow()}
-          </span>
-          <i className="icon-dropdown little-spacer-left" />
-        </button>
+        <Tooltip
+          overlay={<DateTimeFormatter date={this.props.creationDate} />}
+          placement="left"
+          mouseEnterDelay={0.5}>
+          <button
+            className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+            onClick={this.handleClick}>
+            <span className="issue-meta-label">
+              <FormattedRelative value={this.props.creationDate} />
+            </span>
+            <i className="icon-dropdown little-spacer-left" />
+          </button>
+        </Tooltip>
       </BubblePopupHelper>
     );
   }
index 54836503b3958e869bcfa432764f23c502a99bcf..51865ad5850b29c2825a798c81e2737f380750fd 100644 (file)
@@ -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}
         />
         <div className="issue-comment-age">
-          ({moment(comment.createdAt).fromNow()})
+          <FormattedRelative value={comment.createdAt} />
         </div>
         <div className="issue-comment-actions">
           {comment.updatable &&
index 446dc8bcea4a454e5541c226c200c17f090eaaad..65c2c001f3abb3364dff4e2084d983b2ecf39edd 100644 (file)
@@ -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(
     <IssueChangelog
index 9096b72938697989a3ec1a937e553f6899043258..3075e60cc8902d3ede6caead0fae3428d16b6642 100644 (file)
@@ -31,8 +31,6 @@ const comment = {
   updatable: true
 };
 
-jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' }));
-
 it('should render correctly a comment that is not updatable', () => {
   const element = shallow(
     <IssueCommentLine
index 8e1f9850fc8e84b1965a553d665d7759a8749e17..531d5bf7ab29a8ac5c921924c685091b9fe7b585 100644 (file)
@@ -27,20 +27,32 @@ exports[`should open the popup when the button is clicked 2`] = `
   position="bottomright"
   togglePopup={[Function]}
 >
-  <button
-    className="button-link issue-action issue-action-with-options js-issue-show-changelog"
-    onClick={[Function]}
-    title="March 1, 2017 9:36 AM"
+  <Tooltip
+    mouseEnterDelay={0.5}
+    overlay={
+      <DateTimeFormatter
+        date="2017-03-01T09:36:01+0100"
+      />
+    }
+    placement="left"
   >
-    <span
-      className="issue-meta-label"
+    <button
+      className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+      onClick={[Function]}
     >
-      a month ago
-    </span>
-    <i
-      className="icon-dropdown little-spacer-left"
-    />
-  </button>
+      <span
+        className="issue-meta-label"
+      >
+        <FormattedRelative
+          updateInterval={10000}
+          value="2017-03-01T09:36:01+0100"
+        />
+      </span>
+      <i
+        className="icon-dropdown little-spacer-left"
+      />
+    </button>
+  </Tooltip>
 </BubblePopupHelper>
 `;
 
@@ -62,19 +74,31 @@ exports[`should render correctly 1`] = `
   position="bottomright"
   togglePopup={[Function]}
 >
-  <button
-    className="button-link issue-action issue-action-with-options js-issue-show-changelog"
-    onClick={[Function]}
-    title="March 1, 2017 9:36 AM"
+  <Tooltip
+    mouseEnterDelay={0.5}
+    overlay={
+      <DateTimeFormatter
+        date="2017-03-01T09:36:01+0100"
+      />
+    }
+    placement="left"
   >
-    <span
-      className="issue-meta-label"
+    <button
+      className="button-link issue-action issue-action-with-options js-issue-show-changelog"
+      onClick={[Function]}
     >
-      a month ago
-    </span>
-    <i
-      className="icon-dropdown little-spacer-left"
-    />
-  </button>
+      <span
+        className="issue-meta-label"
+      >
+        <FormattedRelative
+          updateInterval={10000}
+          value="2017-03-01T09:36:01+0100"
+        />
+      </span>
+      <i
+        className="icon-dropdown little-spacer-left"
+      />
+    </button>
+  </Tooltip>
 </BubblePopupHelper>
 `;
index 9d4ea6fa3aab3c5f6191e010f341716f1b2d6a3f..a3b7b63d015656aed346648e05c3a16363ffe441 100644 (file)
@@ -42,9 +42,10 @@ exports[`should open the right popups when the buttons are clicked 3`] = `
   <div
     className="issue-comment-age"
   >
-    (
-    a month ago
-    )
+    <FormattedRelative
+      updateInterval={10000}
+      value="2017-03-01T09:36:01+0100"
+    />
   </div>
   <div
     className="issue-comment-actions"
@@ -140,9 +141,10 @@ exports[`should render correctly a comment that is not updatable 1`] = `
   <div
     className="issue-comment-age"
   >
-    (
-    a month ago
-    )
+    <FormattedRelative
+      updateInterval={10000}
+      value="2017-03-01T09:36:01+0100"
+    />
   </div>
   <div
     className="issue-comment-actions"
@@ -180,9 +182,10 @@ exports[`should render correctly a comment that is updatable 1`] = `
   <div
     className="issue-comment-age"
   >
-    (
-    a month ago
-    )
+    <FormattedRelative
+      updateInterval={10000}
+      value="2017-03-01T09:36:01+0100"
+    />
   </div>
   <div
     className="issue-comment-actions"
index 6f0ca9ac054926b89c75d0ccc8c0fa8fe97567e9..01549d5dac77ab29ce491ee6b245a521da00df90 100644 (file)
  */
 // @flow
 import React from 'react';
-import moment from 'moment';
 import { getIssueChangelog } from '../../../api/issues';
 import { translate } from '../../../helpers/l10n';
 import Avatar from '../../../components/ui/Avatar';
 import BubblePopup from '../../../components/common/BubblePopup';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import IssueChangelogDiff from '../components/IssueChangelogDiff';
 /*:: import type { ChangelogDiff } from '../components/IssueChangelogDiff'; */
 /*:: import type { Issue } from '../types'; */
@@ -86,7 +86,7 @@ export default class ChangelogPopup extends React.PureComponent {
             <tbody>
               <tr>
                 <td className="thin text-left text-top nowrap">
-                  {moment(issue.creationDate).format('LLL')}
+                  <DateTimeFormatter date={issue.creationDate} />
                 </td>
                 <td className="text-left text-top">
                   {author ? `${translate('created_by')} ${author}` : translate('created')}
@@ -96,7 +96,7 @@ export default class ChangelogPopup extends React.PureComponent {
               {this.state.changelogs.map((item, idx) =>
                 <tr key={idx}>
                   <td className="thin text-left text-top nowrap">
-                    {moment(item.creationDate).format('LLL')}
+                    <DateTimeFormatter date={item.creationDate} />
                   </td>
                   <td className="text-left text-top">
                     {item.userName &&
index 35d5c05b5f2228a98f93efb37bcef991dc72f354..6c4f9d5977eed1c5d977adfb3ddaa32d081b0316 100644 (file)
@@ -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(
     <ChangelogPopup
index 078a01fc7bfdeb7eec147f5d5d4efeb20740a830..1e649d62abb5c47cb2fc6a8e2e7767f980dc1501 100644 (file)
@@ -15,7 +15,9 @@ exports[`should render the changelog popup correctly 1`] = `
           <td
             className="thin text-left text-top nowrap"
           >
-            March 1, 2017 9:36 AM
+            <DateTimeFormatter
+              date="2017-03-01T09:36:01+0100"
+            />
           </td>
           <td
             className="text-left text-top"
@@ -27,7 +29,9 @@ exports[`should render the changelog popup correctly 1`] = `
           <td
             className="thin text-left text-top nowrap"
           >
-            March 1, 2017 9:36 AM
+            <DateTimeFormatter
+              date="2017-03-01T09:36:01+0100"
+            />
           </td>
           <td
             className="text-left text-top"
diff --git a/server/sonar-web/src/main/js/components/ui/FormattedDate.js b/server/sonar-web/src/main/js/components/ui/FormattedDate.js
deleted file mode 100644 (file)
index f25d003..0000000
+++ /dev/null
@@ -1,50 +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 moment from 'moment';
-
-export default class FormattedDate extends React.PureComponent {
-  /*:: props: {
-    className?: string,
-    date: string | number,
-    format?: string,
-    tooltipFormat?: string
-  };
-*/
-
-  static defaultProps = {
-    format: 'LLL'
-  };
-
-  render() {
-    const { className, date, format, tooltipFormat } = this.props;
-
-    const m = moment(date);
-
-    const title = tooltipFormat ? m.format(tooltipFormat) : undefined;
-
-    return (
-      <time className={className} dateTime={m.format()} title={title}>
-        {m.format(format)}
-      </time>
-    );
-  }
-}
index 670068ed2289abf6e93427b7f9979cfece3e0951..a75ee83fe44b7e55dd31fda1fdb5c070412c6cc3 100644 (file)
  * 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 +
               '<br>' +
               beginning.format('LL') +
-              (isSameDay ? '' : ' – ' + ending.format('LL'))
+              (isSameDay(ending, beginning) ? '' : ' – ' + ending.format('LL'))
             );
           } else {
             return d.text + '<br>' + 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 (file)
index 0000000..455886d
--- /dev/null
@@ -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 (file)
index 3763be4..0000000
+++ /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 (file)
index 0000000..3763be4
--- /dev/null
@@ -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');
+  });
+});
index 982f9375a362e1a009cbb839204ffd6957305ec1..11d7b289cae844dd62aead3ac2f768d6478bdd65 100644 (file)
@@ -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 (file)
index 0000000..5bbb50b
--- /dev/null
@@ -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);
+}
index d457edd9fdbaab5a53e27685605e4b0df94e11ad..ef43101b3325c169f07a56fdd3f0c69b8a34ccc7 100644 (file)
  * 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));
 };
index 708be097e3399b68b9f6320a342065d867dd179f..3af77ae1d6c8ab024655347338c3dc87bf74be68 100644 (file)
  * 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));
 };
index dc607b8dca260761fe29c9c2770e68aee5f10219..ea25726d79f2a5492219a7d6d2853ad70af98c89 100644 (file)
@@ -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 (file)
index 1f5ebda..0000000
+++ /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<string | number> */
-) {
-  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 (file)
index 0000000..57f5194
--- /dev/null
@@ -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 | number>
+): 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<BundleRequestResponse> {
+  const url = '/api/l10n/index';
+  return getJSON(url, params);
+}
+
+export function requestMessages(): Promise<string> {
+  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;
+}
index 0677d81c13cefb3efc23ea877c28ee4f6e672530..4c5ac1c876d720a2e3cc12870559510e5796d1f0 100644 (file)
@@ -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) {
index f7c0f2b6a9cb390d90627b0fabb3fcf30567de6c..a87eefe5e3f5d5d43b8f7f2ce9876da3abb21900 100644 (file)
@@ -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);
   }
 }
 
index deed3501e74d8f97aa507075458797cf3803a631..a093176935548ed04a774cabb64c312bacbcfef7 100644 (file)
@@ -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<void> {
     }, 0);
   });
 }
+
+const intlProvider = new IntlProvider({ locale: 'en' }, {});
+const { intl } = intlProvider.getChildContext();
+export function shallowWithIntl(node, options = {}) {
+  return shallow(node, { ...options, context: { intl, ...options.context } });
+}
index b3a14ca74f1f6753558a1e3119a16c2132f77644..fc6cfa7afabe3840ddae391a5ef8cb86fad635bc 100644 (file)
   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"
index 7875f5ea995325d4d63895b6784c484dd28a02ad..bddfe89e72af0b46afba5d4cc2aa138db8889366 100644 (file)
@@ -1535,6 +1535,7 @@ quality_profiles.list.rules=Rules
 quality_profiles.list.updated=Updated
 quality_profiles.list.used=Used
 quality_profiles.x_activated_out_of_y={0} rules activated out of {1} available
+quality_profiles.x_updated_on_y={0}, updated on {1}
 quality_profiles.change_projects=Change Projects
 quality_profiles.not_found=The requested quality profile was not found.
 quality_profiles.latest_new_rules=Recently Added Rules