From 601e7fbb0ca7cd323b69742e15cd016dac46cf62 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Fri, 3 Dec 2021 18:13:46 +0100 Subject: [PATCH] SONAR-15779 Update react-intl --- server/sonar-web/package.json | 3 +- .../app/components/extensions/Extension.tsx | 4 +- .../ProjectAdminPageExtension-test.tsx.snap | 2 +- .../ProjectPageExtension-test.tsx.snap | 2 +- .../ComponentNavBgTaskNotif-test.tsx | 2 +- .../components/AvailableSinceFacet.tsx | 4 +- .../__snapshots__/FacetsList-test.tsx.snap | 2 +- .../components/LeakPeriodLegend.tsx | 4 +- .../__tests__/LeakPeriodLegend-test.tsx | 4 +- .../__snapshots__/MeasureHeader-test.tsx.snap | 8 +- .../MeasureOverview-test.tsx.snap | 2 +- .../apps/issues/sidebar/CreationDateFacet.tsx | 4 +- .../__tests__/CreationDateFacet-test.tsx | 4 +- .../__snapshots__/Sidebar-test.tsx.snap | 20 +-- .../branches/ProjectLeakPeriodInfo.tsx | 5 +- .../__tests__/ProjectLeakPeriodInfo-test.tsx | 11 +- .../LeakPeriodInfo-test.tsx.snap | 2 +- .../overview/components/LeakPeriodLegend.tsx | 4 +- .../__tests__/LeakPeriodLegend-test.tsx | 4 +- .../main/js/components/controls/DateInput.tsx | 6 +- .../__snapshots__/DateInput-test.tsx.snap | 8 +- .../main/js/components/intl/DateFormatter.tsx | 17 +- .../main/js/components/intl/DateFromNow.tsx | 14 +- .../js/components/intl/DateTimeFormatter.tsx | 7 +- .../main/js/components/intl/TimeFormatter.tsx | 13 +- .../components/intl/__mocks__/DateFromNow.tsx | 4 +- .../intl/__tests__/DateFromNow-test.tsx | 24 ++- .../__snapshots__/DateFromNow-test.tsx.snap | 8 +- .../__tests__/__snapshots__/dateUtils-test.ts | 43 +++++ .../src/main/js/components/intl/dateUtils.ts | 56 +++++++ server/sonar-web/src/main/js/helpers/dates.ts | 3 +- server/sonar-web/src/main/js/helpers/l10n.ts | 10 -- server/sonar-web/src/main/js/types/dates.ts | 21 +++ .../sonar-web/src/main/js/types/extension.ts | 4 +- server/sonar-web/yarn.lock | 147 ++++++++++++------ 35 files changed, 338 insertions(+), 138 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/dateUtils-test.ts create mode 100644 server/sonar-web/src/main/js/components/intl/dateUtils.ts create mode 100644 server/sonar-web/src/main/js/types/dates.ts diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 93e1af6f6d7..8d5c115bc44 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -33,7 +33,7 @@ "react-dom": "16.13.0", "react-draggable": "4.2.0", "react-helmet-async": "1.0.4", - "react-intl": "2.8.0", + "react-intl": "3.12.1", "react-modal": "3.14.3", "react-redux": "5.1.1", "react-router": "3.2.6", @@ -78,7 +78,6 @@ "@types/react": "16.8.23", "@types/react-dom": "16.8.4", "@types/react-helmet": "5.0.15", - "@types/react-intl": "2.3.17", "@types/react-modal": "3.12.1", "@types/react-redux": "6.0.6", "@types/react-router": "3.0.20", diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index bf72c7e3e49..8d4e5dea4ea 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { InjectedIntlProps, injectIntl } from 'react-intl'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; import { connect } from 'react-redux'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { getExtensionStart } from '../../../helpers/extensions'; @@ -31,7 +31,7 @@ import { ExtensionStartMethod } from '../../../types/extension'; import * as theme from '../../theme'; import getStore from '../../utils/getStore'; -interface Props extends InjectedIntlProps { +interface Props extends WrappedComponentProps { currentUser: T.CurrentUser; extension: T.Extension; location: Location; diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap index 8b70ec45509..883107cc708 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly: extension exists 1`] = ` -(FormattedMessage).props(); // Translation key. expect(messageProps.defaultMessage).toBe(expectedMessage); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx index e52c20e49ff..bdf0c0507e2 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { InjectedIntlProps, injectIntl } from 'react-intl'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; import DateInput from '../../../components/controls/DateInput'; import FacetBox from '../../../components/facet/FacetBox'; import FacetHeader from '../../../components/facet/FacetHeader'; @@ -33,7 +33,7 @@ interface Props { value?: Date; } -class AvailableSinceFacet extends React.PureComponent { +class AvailableSinceFacet extends React.PureComponent { handleHeaderClick = () => { this.props.onToggle('availableSince'); }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap index 85aaa9b4e93..7b252f01587 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap @@ -48,7 +48,7 @@ exports[`should render correctly 1`] = ` sansTop25Open={false} sonarsourceSecurityOpen={false} /> - { +export class LeakPeriodLegend extends React.PureComponent { formatDate = (date: string) => { return this.props.intl.formatDate(date, longFormatterOption); }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx index e2adbbcc61d..2ba3b0220a4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx @@ -20,7 +20,7 @@ import { differenceInDays } from 'date-fns'; import { shallow } from 'enzyme'; import * as React from 'react'; -import { InjectedIntlProps } from 'react-intl'; +import { IntlShape } from 'react-intl'; import { LeakPeriodLegend } from '../LeakPeriodLegend'; jest.mock('date-fns', () => { @@ -72,7 +72,7 @@ function getWrapper(component: T.ComponentMeasure, period: T.Period) { return shallow( x } as InjectedIntlProps['intl']} + intl={{ formatDate: (x: any) => x } as IntlShape} period={period} /> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap index 2e025bedaf5..643a2bb9dd8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap @@ -51,7 +51,7 @@ exports[`should render correctly 1`] = `
- - - - - | undefined; } -export class CreationDateFacet extends React.PureComponent { +export class CreationDateFacet extends React.PureComponent { property = 'createdAt'; static defaultProps = { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx index 2dfa5ea4b7d..c1bf31a3299 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { InjectedIntlProps } from 'react-intl'; +import { IntlShape } from 'react-intl'; import { mockComponent } from '../../../../helpers/mocks/component'; import { ComponentQualifier } from '../../../../types/component'; import { CreationDateFacet } from '../CreationDateFacet'; @@ -68,7 +68,7 @@ function shallowRender(props?: Partial) { intl={ { formatDate: (date: string) => 'formatted.' + date - } as InjectedIntlProps['intl'] + } as IntlShape } onChange={jest.fn()} onToggle={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap index df5f5febae0..435a8ee0bcd 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap @@ -8,7 +8,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -24,7 +24,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -42,7 +42,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -61,7 +61,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -80,7 +80,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -98,7 +98,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -116,7 +116,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -134,7 +134,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -152,7 +152,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", @@ -171,7 +171,7 @@ Array [ "ResolutionFacet", "StatusFacet", "StandardFacet", - "InjectIntl(CreationDateFacet)", + "injectIntl(CreationDateFacet)", "Connect(LanguageFacet)", "RuleFacet", "TagFacet", diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx index 8c47ec3485d..1996f07ae95 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx @@ -18,15 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { InjectedIntl, injectIntl } from 'react-intl'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; import { longFormatterOption } from '../../../components/intl/DateFormatter'; import DateFromNow from '../../../components/intl/DateFromNow'; import { formatterOption } from '../../../components/intl/DateTimeFormatter'; import { translateWithParameters } from '../../../helpers/l10n'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; -export interface ProjectLeakPeriodInfoProps { - intl: Pick; +export interface ProjectLeakPeriodInfoProps extends WrappedComponentProps { leakPeriod: T.Period; } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx index eb3a9d35711..efe108afacd 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx @@ -20,6 +20,7 @@ import { differenceInDays } from 'date-fns'; import { shallow } from 'enzyme'; import * as React from 'react'; +import { IntlShape } from 'react-intl'; import { mockPeriod } from '../../../../helpers/testMocks'; import { ProjectLeakPeriodInfo } from '../ProjectLeakPeriodInfo'; @@ -63,10 +64,12 @@ it('should render a more precise date', () => { function shallowRender(period: Partial = {}) { return shallow( 'formatted.' + date, - formatTime: (date: string) => 'formattedTime.' + date - }} + intl={ + { + formatDate: (date: string) => 'formatted.' + date, + formatTime: (date: string) => 'formattedTime.' + date + } as IntlShape + } leakPeriod={mockPeriod({ ...period })} /> ); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/LeakPeriodInfo-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/LeakPeriodInfo-test.tsx.snap index a6a8d910dfa..1feb0477f15 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/LeakPeriodInfo-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/LeakPeriodInfo-test.tsx.snap @@ -13,7 +13,7 @@ exports[`renders correctly for applications 1`] = ` `; exports[`renders correctly for projects 1`] = ` - = { SPECIFIC_ANALYSIS: true }; -export class LeakPeriodLegend extends React.PureComponent { +export class LeakPeriodLegend extends React.PureComponent { formatDate = (date: string) => { return this.props.intl.formatDate(date, longFormatterOption); }; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx index b1ee6be8e1e..76929c951e9 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx @@ -20,7 +20,7 @@ import { differenceInDays } from 'date-fns'; import { shallow } from 'enzyme'; import * as React from 'react'; -import { InjectedIntlProps } from 'react-intl'; +import { IntlShape } from 'react-intl'; import { LeakPeriodLegend } from '../LeakPeriodLegend'; jest.mock('date-fns', () => { @@ -67,7 +67,7 @@ function getWrapper(period: Partial = {}) { { formatDate: (date: string) => 'formatted.' + date, formatTime: (date: string) => 'formattedTime.' + date - } as InjectedIntlProps['intl'] + } as IntlShape } period={{ date: '2013-09-22T00:00:00+0200', diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index de7cb47b4d9..c0e0517ceeb 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -22,7 +22,7 @@ import { addMonths, setMonth, setYear, subMonths } from 'date-fns'; import { range } from 'lodash'; import * as React from 'react'; import { DayModifiers, Modifier, Modifiers } from 'react-day-picker'; -import { InjectedIntlProps, injectIntl } from 'react-intl'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; import { ButtonIcon, ClearButton } from '../../components/controls/buttons'; import OutsideClickHandler from '../../components/controls/OutsideClickHandler'; import CalendarIcon from '../../components/icons/CalendarIcon'; @@ -149,7 +149,7 @@ export default class DateInput extends React.PureComponent { className={classNames('date-input-control-input', this.props.inputClassName, { 'is-filled': this.props.value !== undefined })} - innerRef={node => (this.input = node)} + innerRef={(node: HTMLInputElement | null) => (this.input = node)} name={this.props.name} onFocus={this.openCalendar} placeholder={this.props.placeholder} @@ -218,7 +218,7 @@ function NullComponent() { } type InputWrapperProps = T.Omit, 'value'> & - InjectedIntlProps & { + WrappedComponentProps & { innerRef: React.Ref; value: Date | undefined; }; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap index 457d6cf9d2b..0cde2461ed5 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap @@ -7,7 +7,7 @@ exports[`should render 1`] = ` - - - - React.ReactNode; - date: DateSource; + date: ParsableDate; long?: boolean; } -export const formatterOption = { year: 'numeric', month: 'short', day: '2-digit' }; +export const formatterOption: FormatDateOptions = { + year: 'numeric', + month: 'short', + day: '2-digit' +}; -export const longFormatterOption = { year: 'numeric', month: 'long', day: 'numeric' }; +export const longFormatterOption: FormatDateOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' +}; export default function DateFormatter({ children, date, long }: DateFormatterProps) { return ( diff --git a/server/sonar-web/src/main/js/components/intl/DateFromNow.tsx b/server/sonar-web/src/main/js/components/intl/DateFromNow.tsx index 713b31fdf8d..13fc3d407a6 100644 --- a/server/sonar-web/src/main/js/components/intl/DateFromNow.tsx +++ b/server/sonar-web/src/main/js/components/intl/DateFromNow.tsx @@ -19,14 +19,16 @@ */ import { differenceInHours } from 'date-fns'; import * as React from 'react'; -import { DateSource, FormattedRelative } from 'react-intl'; +import { FormattedRelativeTime } from 'react-intl'; import { parseDate } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; +import { ParsableDate } from '../../types/dates'; import DateTimeFormatter from './DateTimeFormatter'; +import { getRelativeTimeProps } from './dateUtils'; export interface DateFromNowProps { children?: (formattedDate: string) => React.ReactNode; - date?: DateSource; + date?: ParsableDate; hourPrecision?: boolean; } @@ -43,17 +45,21 @@ export default function DateFromNow(props: DateFromNowProps) { return <>{originalChildren(translate('never'))}; } - if (date && hourPrecision && differenceInHours(Date.now(), date) < 1) { + if (hourPrecision && differenceInHours(Date.now(), date) < 1) { children = () => originalChildren(translate('less_than_1_hour_ago')); } const parsedDate = parseDate(date); + const relativeTimeProps = getRelativeTimeProps(date); + return ( {formattedDate => ( - {children} + + {children as FormattedRelativeTime['props']['children']} + )} diff --git a/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx index 64394b80c58..8390bfbfca1 100644 --- a/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx +++ b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx @@ -18,15 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { DateSource, FormattedDate } from 'react-intl'; +import { FormatDateOptions, FormattedDate } from 'react-intl'; import { parseDate } from '../../helpers/dates'; +import { ParsableDate } from '../../types/dates'; interface Props { children?: (formattedDate: string) => React.ReactNode; - date: DateSource; + date: ParsableDate; } -export const formatterOption = { +export const formatterOption: FormatDateOptions = { year: 'numeric', month: 'long', day: 'numeric', diff --git a/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx index 8fb5af05440..d35bcd966e6 100644 --- a/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx +++ b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx @@ -18,18 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { DateSource, FormattedTime } from 'react-intl'; +import { FormatDateOptions, FormattedTime } from 'react-intl'; import { parseDate } from '../../helpers/dates'; +import { ParsableDate } from '../../types/dates'; export interface TimeFormatterProps { children?: (formattedDate: string) => React.ReactNode; - date: DateSource; + date: ParsableDate; long?: boolean; } -export const formatterOption = { hour: 'numeric', minute: 'numeric' }; +export const formatterOption: FormatDateOptions = { hour: 'numeric', minute: 'numeric' }; -export const longFormatterOption = { hour: 'numeric', minute: 'numeric', second: 'numeric' }; +export const longFormatterOption: FormatDateOptions = { + hour: 'numeric', + minute: 'numeric', + second: 'numeric' +}; export default function TimeFormatter({ children, date, long }: TimeFormatterProps) { return ( diff --git a/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx b/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx index 76a585b4045..b06a43ee329 100644 --- a/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx +++ b/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { DateSource } from 'react-intl'; +import { ParsableDate } from '../../../types/dates'; interface Props { children?: (formattedDate: string) => React.ReactNode; - date: DateSource; + date: ParsableDate; } export default function DateFromNow({ children, date }: Props) { diff --git a/server/sonar-web/src/main/js/components/intl/__tests__/DateFromNow-test.tsx b/server/sonar-web/src/main/js/components/intl/__tests__/DateFromNow-test.tsx index 901b5cbba0c..216daccdeb0 100644 --- a/server/sonar-web/src/main/js/components/intl/__tests__/DateFromNow-test.tsx +++ b/server/sonar-web/src/main/js/components/intl/__tests__/DateFromNow-test.tsx @@ -19,12 +19,16 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { FormattedRelative, IntlProvider } from 'react-intl'; +import { FormattedRelativeTime, IntlProvider } from 'react-intl'; import DateFromNow, { DateFromNowProps } from '../DateFromNow'; import DateTimeFormatter from '../DateTimeFormatter'; const date = '2020-02-20T20:20:20Z'; +jest.mock('../dateUtils', () => ({ + getRelativeTimeProps: jest.fn().mockReturnValue({ value: -1, unit: 'year' }) +})); + it('should render correctly', () => { const wrapper = shallowRender(); @@ -43,24 +47,30 @@ it('should render correctly when there is no date', () => { it('should render correctly when the date is less than one hour in the past', () => { const veryCloseDate = new Date(date); veryCloseDate.setMinutes(veryCloseDate.getMinutes() - 10); - jest.spyOn(Date, 'now').mockImplementation(() => (new Date(date) as unknown) as number); + const mockDateNow = jest + .spyOn(Date, 'now') + .mockImplementation(() => (new Date(date) as unknown) as number); const children = jest.fn(); shallowRender({ date: veryCloseDate, hourPrecision: true }, children) - .dive() - .dive() - .find(FormattedRelative) + .dive() // into DateTimeFormatter + .dive() // into DateFormatter + .dive() // into rendering function + .find(FormattedRelativeTime) .props().children!(date); expect(children).toHaveBeenCalledWith('less_than_1_hour_ago'); + mockDateNow.mockRestore(); }); function shallowRender(overrides: Partial = {}, children: jest.Mock = jest.fn()) { - return shallow( + return shallow( {formattedDate => children(formattedDate)} - ).dive(); + ) + .dive() // into the ContextProvider generated by IntlProvider + .dive(); // into the DateFromNow we actually want to render } diff --git a/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap b/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap index 19ee5460047..f893ec98cf7 100644 --- a/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap @@ -12,11 +12,11 @@ exports[`should render correctly: children 1`] = ` - [Function] - + `; diff --git a/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/dateUtils-test.ts b/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/dateUtils-test.ts new file mode 100644 index 00000000000..b5187f37959 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/__tests__/__snapshots__/dateUtils-test.ts @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { getRelativeTimeProps } from '../../dateUtils'; + +const mockDateNow = jest.spyOn(Date, 'now'); + +describe('getRelativeTimeProps', () => { + mockDateNow.mockImplementation(() => new Date('2021-02-20T20:20:20Z').getTime()); + + it.each([ + ['year', '2020-02-19T20:20:20Z', -1], + ['month', '2020-11-18T20:20:20Z', -3], + ['day', '2021-02-18T18:20:20Z', -2] + ])('should return the correct props for dates older than a %s', (unit, date, value) => { + expect(getRelativeTimeProps(date)).toEqual({ value, unit }); + }); + + it('should return the correct props for dates from less than a day ago', () => { + expect(getRelativeTimeProps('2021-02-20T20:19:45Z')).toEqual({ + value: -35, + unit: 'second', + updateIntervalInSeconds: 10 + }); + }); +}); diff --git a/server/sonar-web/src/main/js/components/intl/dateUtils.ts b/server/sonar-web/src/main/js/components/intl/dateUtils.ts new file mode 100644 index 00000000000..390d9d729b3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/dateUtils.ts @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { + differenceInDays, + differenceInMonths, + differenceInSeconds, + differenceInYears +} from 'date-fns'; +import { FormattedRelativeTime } from 'react-intl'; +import { ParsableDate } from '../../types/dates'; + +const UPDATE_INTERVAL_IN_SECONDS = 10; + +export function getRelativeTimeProps( + date: ParsableDate +): Pick { + const y = differenceInYears(date, Date.now()); + + if (Math.abs(y) > 0) { + return { value: y, unit: 'year' }; + } + + const m = differenceInMonths(date, Date.now()); + if (Math.abs(m) > 0) { + return { value: m, unit: 'month' }; + } + + const d = differenceInDays(date, Date.now()); + if (Math.abs(d) > 0) { + return { value: d, unit: 'day' }; + } + + return { + value: differenceInSeconds(date, Date.now()), + unit: 'second', + updateIntervalInSeconds: UPDATE_INTERVAL_IN_SECONDS + }; +} diff --git a/server/sonar-web/src/main/js/helpers/dates.ts b/server/sonar-web/src/main/js/helpers/dates.ts index f7944f3955a..1e1b23d5d78 100644 --- a/server/sonar-web/src/main/js/helpers/dates.ts +++ b/server/sonar-web/src/main/js/helpers/dates.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { parse } from 'date-fns'; +import { ParsableDate } from '../types/dates'; function pad(number: number) { if (number < 10) { @@ -26,8 +27,6 @@ function pad(number: number) { return number; } -type ParsableDate = string | number | Date; - export function parseDate(rawDate: ParsableDate): Date { return parse(rawDate); } diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts index a96f34682c2..a57732333ca 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.ts +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -147,16 +147,6 @@ export async function loadL10nBundle() { resetCurrentLocale(bundle.locale); resetMessages(bundle.messages); - // No need to load english (default) bundle, it's coming with react-intl - if (bundle.locale !== DEFAULT_LOCALE) { - const [intlBundle, intl] = await Promise.all([ - import(`react-intl/locale-data/${bundle.locale}`), - import('react-intl') - ]); - - intl.addLocaleData(intlBundle.default); - } - return bundle; } diff --git a/server/sonar-web/src/main/js/types/dates.ts b/server/sonar-web/src/main/js/types/dates.ts new file mode 100644 index 00000000000..8b221464815 --- /dev/null +++ b/server/sonar-web/src/main/js/types/dates.ts @@ -0,0 +1,21 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 ParsableDate = string | number | Date; diff --git a/server/sonar-web/src/main/js/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts index 624da3e7793..38d0432c267 100644 --- a/server/sonar-web/src/main/js/types/extension.ts +++ b/server/sonar-web/src/main/js/types/extension.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { InjectedIntl } from 'react-intl'; +import { IntlShape } from 'react-intl'; import { Store as ReduxStore } from 'redux'; import { Location, Router } from '../components/hoc/withRouter'; import { Store } from '../store/rootReducer'; @@ -35,7 +35,7 @@ export interface ExtensionStartMethodParameter { store: ReduxStore; el: HTMLElement | undefined | null; currentUser: T.CurrentUser; - intl: InjectedIntl; + intl: IntlShape; location: Location; router: Router; theme: { diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 3dc3d7dfd3b..51541fd6a9c 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -1997,6 +1997,49 @@ __metadata: languageName: node linkType: hard +"@formatjs/intl-displaynames@npm:^1.2.0": + version: 1.2.10 + resolution: "@formatjs/intl-displaynames@npm:1.2.10" + dependencies: + "@formatjs/intl-utils": ^2.3.0 + checksum: 25320e00c383260c1c3c44bd8be017b8ebd1b1b7de4188d05934aa40b65adda02b102eba46bf4e01e06f9db79d2d8663a720afe7f2ee69495a3022055bea4810 + languageName: node + linkType: hard + +"@formatjs/intl-listformat@npm:^1.4.1": + version: 1.4.8 + resolution: "@formatjs/intl-listformat@npm:1.4.8" + dependencies: + "@formatjs/intl-utils": ^2.3.0 + checksum: 1e5b2ef45b7e0143fb4c809178aed00ad1d1dfbcba25c339bf54bdd5e35acee6c72a25bd30189812a3211103a58a7e0800e49bc3e973f89bc5e80c41da38f6e1 + languageName: node + linkType: hard + +"@formatjs/intl-relativetimeformat@npm:^4.5.9": + version: 4.5.16 + resolution: "@formatjs/intl-relativetimeformat@npm:4.5.16" + dependencies: + "@formatjs/intl-utils": ^2.3.0 + checksum: 466268cb4f3c326b222cc0f79b176949d4cc79e29d11fe6e8d003b89b3495018728d55ba25189f3856b88c0f5657a57365f039504c32ea78a8fb555ff80e9580 + languageName: node + linkType: hard + +"@formatjs/intl-unified-numberformat@npm:^3.2.0": + version: 3.3.7 + resolution: "@formatjs/intl-unified-numberformat@npm:3.3.7" + dependencies: + "@formatjs/intl-utils": ^2.3.0 + checksum: dae9c855d8b36b833ee9a71e63b13240dabc9b84ed13192411f06ac903a5c2fb002fd4736d7b71df73c4c776792255c7b2deedb94c5cddc12967fcb7c14f6133 + languageName: node + linkType: hard + +"@formatjs/intl-utils@npm:^2.2.0, @formatjs/intl-utils@npm:^2.3.0": + version: 2.3.0 + resolution: "@formatjs/intl-utils@npm:2.3.0" + checksum: a7a6339dac796bccd738b3f0425863c79951156c5b61ed804869bd2ba064544badf3ec0bad576eb56fdbaf11585d99b8a089522a9b5829ba0f99a85d33222cfb + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1": version: 1.1.2 resolution: "@gar/promisify@npm:1.1.2" @@ -2571,6 +2614,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.1 + resolution: "@types/hoist-non-react-statics@npm:3.3.1" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: 2c0778570d9a01d05afabc781b32163f28409bb98f7245c38d5eaf082416fdb73034003f5825eb5e21313044e8d2d9e1f3fe2831e345d3d1b1d20bcd12270719 + languageName: node + linkType: hard + "@types/htmlparser2@npm:*": version: 3.10.0 resolution: "@types/htmlparser2@npm:3.10.0" @@ -2582,6 +2635,13 @@ __metadata: languageName: node linkType: hard +"@types/invariant@npm:^2.2.31": + version: 2.2.35 + resolution: "@types/invariant@npm:2.2.35" + checksum: af1b624057c89789ed0917838fea3d42bb0c101cc22b829a24d8777c678be3bc79d6ae05992a13bdf607b94731262467a2e62a809602ea1f7eea5e8c2242660d + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.1 resolution: "@types/istanbul-lib-coverage@npm:2.0.1" @@ -2745,13 +2805,6 @@ __metadata: languageName: node linkType: hard -"@types/react-intl@npm:2.3.17": - version: 2.3.17 - resolution: "@types/react-intl@npm:2.3.17" - checksum: 6d6169a7d41d87552074bfdf0f42ee229b7f48eb9bc1d99205c39693bdc006cbb2a5790954c2087cbe4ca20cf027e992bfe60de8ae9e15d71700e8742541b470 - languageName: node - linkType: hard - "@types/react-modal@npm:3.12.1": version: 3.12.1 resolution: "@types/react-modal@npm:3.12.1" @@ -3016,7 +3069,6 @@ __metadata: "@types/react": 16.8.23 "@types/react-dom": 16.8.4 "@types/react-helmet": 5.0.15 - "@types/react-intl": 2.3.17 "@types/react-modal": 3.12.1 "@types/react-redux": 6.0.6 "@types/react-router": 3.0.20 @@ -3087,7 +3139,7 @@ __metadata: react-dom: 16.13.0 react-draggable: 4.2.0 react-helmet-async: 1.0.4 - react-intl: 2.8.0 + react-intl: 3.12.1 react-modal: 3.14.3 react-redux: 5.1.1 react-router: 3.2.6 @@ -7353,7 +7405,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -7687,39 +7739,33 @@ fsevents@~2.3.2: languageName: node linkType: hard -"intl-format-cache@npm:^2.0.5": - version: 2.2.9 - resolution: "intl-format-cache@npm:2.2.9" - checksum: 70ebb4fe2e3005d718562eff56a9f19d29ace8f51d6e549abe57ac9bd8a6791ace905f7d0d2a870236552746eff24793e782d64a3d29b93d3ff2e9bca0e3d6d8 - languageName: node - linkType: hard - -"intl-messageformat-parser@npm:1.4.0": - version: 1.4.0 - resolution: "intl-messageformat-parser@npm:1.4.0" - checksum: 0606b78523a9730b47292dbf1f1835e189120cd91aa742d1c0925ce0925fb067233fc74ec259a20d7810f5a1a0bd801f5884fe04493082596cf578bfeafa34c6 +"intl-format-cache@npm:^4.2.21": + version: 4.3.1 + resolution: "intl-format-cache@npm:4.3.1" + checksum: d55581edaba0d083a3ff26a46e2ed953c434420918e61991db78140e3e0a7db14f924f195fcd0c01cf3de65cb18dda0c549d35f683e01dd8f062d27fe0524fae languageName: node linkType: hard -"intl-messageformat@npm:^2.0.0, intl-messageformat@npm:^2.1.0": - version: 2.2.0 - resolution: "intl-messageformat@npm:2.2.0" +"intl-messageformat-parser@npm:^3.6.4": + version: 3.6.4 + resolution: "intl-messageformat-parser@npm:3.6.4" dependencies: - intl-messageformat-parser: 1.4.0 - checksum: 5562de75dddae69546180403fec196ed6564d41cb82964de8e319bd9fa9edfe5aaa581bc40775815b65fbdab3db96b62bb0757c2a936ac025e705333e4bd82cd + "@formatjs/intl-unified-numberformat": ^3.2.0 + checksum: 69e781b6fec47f1fe5b2dc9abba79ac74b8cb4e9b40da4acc3ef2e9c6140d3d90070fd2c055d16e48c3a8bce626bb1f547ad1e29df772aff468a377658e70e6e languageName: node linkType: hard -"intl-relativeformat@npm:^2.1.0": - version: 2.2.0 - resolution: "intl-relativeformat@npm:2.2.0" +"intl-messageformat@npm:^7.8.4": + version: 7.8.4 + resolution: "intl-messageformat@npm:7.8.4" dependencies: - intl-messageformat: ^2.0.0 - checksum: 887862279d7ad415f9fefa59b5abb48878f08214b7d4592e4834c956e5317b258d1103ba1c353284c213dcc574a98b5f65075323f50c94632ec79f7c5cb5c2cd + intl-format-cache: ^4.2.21 + intl-messageformat-parser: ^3.6.4 + checksum: a24fd5763c3aae450d97c4a67d95618d791f7a564996cbf6f1c4c5433c3c6fe0c141d967f94775bf9c0e70ce7791c66854987fa03a99a57f10c96186ddd90658 languageName: node linkType: hard -"invariant@npm:^2.1.1, invariant@npm:^2.2.1, invariant@npm:^2.2.2, invariant@npm:^2.2.4": +"invariant@npm:^2.2.1, invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -11692,19 +11738,25 @@ fsevents@~2.3.2: languageName: node linkType: hard -"react-intl@npm:2.8.0": - version: 2.8.0 - resolution: "react-intl@npm:2.8.0" - dependencies: - hoist-non-react-statics: ^2.5.5 - intl-format-cache: ^2.0.5 - intl-messageformat: ^2.1.0 - intl-relativeformat: ^2.1.0 - invariant: ^2.1.1 +"react-intl@npm:3.12.1": + version: 3.12.1 + resolution: "react-intl@npm:3.12.1" + dependencies: + "@formatjs/intl-displaynames": ^1.2.0 + "@formatjs/intl-listformat": ^1.4.1 + "@formatjs/intl-relativetimeformat": ^4.5.9 + "@formatjs/intl-unified-numberformat": ^3.2.0 + "@formatjs/intl-utils": ^2.2.0 + "@types/hoist-non-react-statics": ^3.3.1 + "@types/invariant": ^2.2.31 + hoist-non-react-statics: ^3.3.2 + intl-format-cache: ^4.2.21 + intl-messageformat: ^7.8.4 + intl-messageformat-parser: ^3.6.4 + shallow-equal: ^1.2.1 peerDependencies: - prop-types: ^15.5.4 - react: ^0.14.9 || ^15.0.0 || ^16.0.0 - checksum: c23ff0b895af8c563435ccace81d8eb32c27e078fad9cf10a44f4409b53c9e4eab08ec7ebc25a17a0ff60d8542b745aff8ee3c323aeaf6ed2dae1b27d7d47840 + react: ^16.3.0 + checksum: 4a9e9101d8d5bd60c73713b99bdad2deb2b6b21fd953f16165b5c4be64aafd503418e009736ba986ca75756c08ad41b80991090e222d80b2bebfefb027bf11eb languageName: node linkType: hard @@ -12779,6 +12831,13 @@ resolve@1.1.7: languageName: node linkType: hard +"shallow-equal@npm:^1.2.1": + version: 1.2.1 + resolution: "shallow-equal@npm:1.2.1" + checksum: 4f1645cc516e7754c4438db687e1da439a5f29a7dba2ba90c5f88e5708aeb17bc4355ba45cad805b0e95dc898e37d8bf6d77d854919c7512f89939986cff8cd1 + languageName: node + linkType: hard + "shallowequal@npm:^1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" -- 2.39.5