diff --git a/src/elements/common/date/DateField.js b/src/elements/common/date/DateField.js.flow
similarity index 75%
rename from src/elements/common/date/DateField.js
rename to src/elements/common/date/DateField.js.flow
index 5f3c58ff60..9e8f9485b9 100644
--- a/src/elements/common/date/DateField.js
+++ b/src/elements/common/date/DateField.js.flow
@@ -1,12 +1,5 @@
-/**
- * @flow
- * @file Function to render the date table cell
- * @author Box
- */
-
import * as React from 'react';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import type { IntlShape } from 'react-intl';
+import { FormattedMessage, useIntl} from 'react-intl';
import { isToday, isYesterday } from '../../../utils/datetime';
import messages from '../messages';
import './DateField.scss';
@@ -14,10 +7,9 @@ import './DateField.scss';
type Props = {
capitalize?: boolean,
date: string,
- dateFormat?: Object,
- intl: IntlShape,
+ dateFormat?: any,
omitCommas?: boolean,
- relative?: boolean,
+ relative?: boolean
};
const DEFAULT_DATE_FORMAT = {
@@ -31,10 +23,10 @@ const DateField = ({
date,
dateFormat = DEFAULT_DATE_FORMAT,
omitCommas = false,
- intl,
relative = true,
capitalize = false,
}: Props) => {
+ const { formatDate } = useIntl();
const d = new Date(date);
const isTodaysDate = isToday(d);
const isYesterdaysDate = isYesterday(d);
@@ -52,9 +44,9 @@ const DateField = ({
return Message;
}
- let formattedDate = intl.formatDate(d, dateFormat);
+ let formattedDate = formatDate(d, dateFormat);
formattedDate = omitCommas ? formattedDate.replace(/,/g, '') : formattedDate;
return formattedDate;
};
-export default injectIntl(DateField);
+export default DateField;
diff --git a/src/elements/common/date/DateField.tsx b/src/elements/common/date/DateField.tsx
new file mode 100644
index 0000000000..35b5107fba
--- /dev/null
+++ b/src/elements/common/date/DateField.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react';
+import { FormatDateOptions, FormattedMessage, useIntl } from 'react-intl';
+import { isToday, isYesterday } from '../../../utils/datetime';
+
+import messages from '../messages';
+import './DateField.scss';
+
+const DEFAULT_DATE_FORMAT = {
+ weekday: 'short',
+ month: 'short',
+ year: 'numeric',
+ day: 'numeric',
+} as const;
+
+export interface DateFieldProps {
+ capitalize?: boolean;
+ date: string;
+ dateFormat?: FormatDateOptions;
+ omitCommas?: boolean;
+ relative?: boolean;
+}
+
+const DateField = ({
+ date,
+ dateFormat = DEFAULT_DATE_FORMAT,
+ omitCommas = false,
+ relative = true,
+ capitalize = false,
+}: DateFieldProps): React.ReactNode | string => {
+ const { formatDate } = useIntl();
+ const d = new Date(date);
+ const isTodaysDate = isToday(d);
+ const isYesterdaysDate = isYesterday(d);
+
+ if (relative && (isTodaysDate || isYesterdaysDate)) {
+ let Message = ;
+ if (isYesterdaysDate) {
+ Message = ;
+ }
+
+ if (capitalize) {
+ return {Message};
+ }
+
+ return Message;
+ }
+
+ let formattedDate = formatDate(d, dateFormat);
+ formattedDate = omitCommas ? formattedDate.replace(/,/g, '') : formattedDate;
+ return formattedDate;
+};
+
+export default DateField;
diff --git a/src/elements/common/date/__tests__/DateField.test.tsx b/src/elements/common/date/__tests__/DateField.test.tsx
new file mode 100644
index 0000000000..48661a4426
--- /dev/null
+++ b/src/elements/common/date/__tests__/DateField.test.tsx
@@ -0,0 +1,48 @@
+import * as React from 'react';
+import { render, screen } from '../../../../test-utils/testing-library';
+
+import DateField, { DateFieldProps } from '../DateField';
+
+describe('elements/common/date/DateField', () => {
+ const renderComponent = (props: Partial = {}) => {
+ render();
+ };
+ test('renders formatted date', () => {
+ renderComponent();
+ expect(screen.getByText('Tue, Oct 10, 2023')).toBeInTheDocument();
+ });
+
+ test("renders today message for today's date", () => {
+ renderComponent({ date: new Date().toISOString(), relative: true });
+ expect(screen.getByText('today')).toBeInTheDocument();
+ });
+
+ test("renders yesterday message for yesterday's date", () => {
+ const yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ renderComponent({ date: yesterday.toISOString(), relative: true });
+ expect(screen.getByText('yesterday')).toBeInTheDocument();
+ });
+
+ test('renders formatted date without commas', () => {
+ renderComponent({ omitCommas: true });
+ expect(screen.getByText('Tue Oct 10 2023')).toBeInTheDocument();
+ });
+
+ test('renders capitalized today message', () => {
+ renderComponent({ date: new Date().toISOString(), relative: true, capitalize: true });
+ expect(screen.getByText('today')).toHaveClass('be-date-capitalize');
+ });
+
+ test('renders capitalized yesterday message', () => {
+ const yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ renderComponent({ date: yesterday.toISOString(), relative: true, capitalize: true });
+ expect(screen.getByText('yesterday')).toHaveClass('be-date-capitalize');
+ });
+
+ test('renders formatted date with custom format', () => {
+ renderComponent({ dateFormat: { year: '2-digit', month: '2-digit', day: '2-digit' } });
+ expect(screen.getByText('10/10/23')).toBeInTheDocument();
+ });
+});
diff --git a/src/elements/common/date/index.js.flow b/src/elements/common/date/index.js.flow
new file mode 100644
index 0000000000..69e90fcd34
--- /dev/null
+++ b/src/elements/common/date/index.js.flow
@@ -0,0 +1 @@
+export {default} from './DateField';
diff --git a/src/elements/common/date/index.js b/src/elements/common/date/index.ts
similarity index 81%
rename from src/elements/common/date/index.js
rename to src/elements/common/date/index.ts
index cfabefaa6c..5966e1b747 100644
--- a/src/elements/common/date/index.js
+++ b/src/elements/common/date/index.ts
@@ -1,2 +1 @@
-// @flow
export { default } from './DateField';
diff --git a/src/utils/__tests__/datetime.test.js b/src/utils/__tests__/datetime.test.ts
similarity index 96%
rename from src/utils/__tests__/datetime.test.js
rename to src/utils/__tests__/datetime.test.ts
index ea1257953a..07f64b2fc5 100644
--- a/src/utils/__tests__/datetime.test.js
+++ b/src/utils/__tests__/datetime.test.ts
@@ -140,7 +140,7 @@ describe('utils/datetime', () => {
['2019-01-01T09:41:56-07:00', true],
['some random string', false],
['', false],
- ])('should interpret %s as a %p date', (dateString, expected) => {
+ ])('should interpret %s as a %p date', (dateString: string, expected: boolean) => {
const date = new Date(dateString);
expect(isValidDate(date)).toBe(expected);
});
@@ -151,7 +151,7 @@ describe('utils/datetime', () => {
const TEN_MIN_IN_MS = 600000;
const date = new Date('1995-12-17T03:24:00');
const result = addTime(date, TEN_MIN_IN_MS);
- expect(result.getMinutes()).toBe(34);
+ expect((result as Date).getMinutes()).toBe(34);
});
test('should correctly add time if the date Value is a number', () => {
@@ -168,7 +168,7 @@ describe('utils/datetime', () => {
['2018-06-13T01:00:00.000+01:00', '2018-06-13T07:00:00.000Z'],
['2018-06-12T23:00:00.000-0100', '2018-06-13T07:00:00.000Z'],
['2018-06-13T02:00:00.000+02', '2018-06-13T07:00:00.000Z'],
- ])('should correctly convert from %s to %s', (originDateTime, expectedDateTime) => {
+ ])('should correctly convert from %s to %s', (originDateTime: string, expectedDateTime: string) => {
const result = convertISOStringToUTCDate(originDateTime);
expect(result.toISOString()).toBe(expectedDateTime);
});
@@ -202,7 +202,7 @@ describe('utils/datetime', () => {
// Null-conversion examples
['2018-06-13T00:00:00.000-05:45', '2018-06-13T00:00:00.000-05:45'],
['2018-06-13T00:00:00.000+34', '2018-06-13T00:00:00.000+34'],
- ])('should convert %s to %s correctly', (from, to) => {
+ ])('should convert %s to %s correctly', (from: string, to: string) => {
const input = convertISOStringtoRFC3339String(from);
expect(input).toEqual(to);
});
diff --git a/src/utils/datetime.js b/src/utils/datetime.js.flow
similarity index 99%
rename from src/utils/datetime.js
rename to src/utils/datetime.js.flow
index 03dc921e4e..cac050c5f2 100644
--- a/src/utils/datetime.js
+++ b/src/utils/datetime.js.flow
@@ -1,8 +1,3 @@
-/**
- * @flow
- * @file Date and time utilities
- * @author Box
- */
import isNaN from 'lodash/isNaN';
const MILLISECONDS_PER_SECOND = 1000;
diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts
new file mode 100644
index 0000000000..18590b1ec6
--- /dev/null
+++ b/src/utils/datetime.ts
@@ -0,0 +1,251 @@
+import isNaN from 'lodash/isNaN';
+
+const MILLISECONDS_PER_SECOND = 1000;
+// 24 hours * 60 minutes * 60 seconds * 1000 milliseconds
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * MILLISECONDS_PER_SECOND;
+// 60 sec * 1000
+const MILLISECONDS_PER_MINUTE = 60 * MILLISECONDS_PER_SECOND;
+
+/**
+ * RegExp matcher for acceptable ISO 8601 date formats w/ timezone (see below)
+ * Capture groups structured as follows:
+ * 1) the date/time portion (2018-06-13T00:00:00.000)
+ * 2) the milliseconds (if matched)
+ * 3) the timezone portion (e.g., Z, +03, -0400, +05:00)
+ * 4) the Z format for timezone (if matched)
+ * 5) the short format for timezone (if matched)
+ * 6) the colon-less format for timezone (if matched)
+ * 7) the colon long format for timezone (if matched)
+ */
+const RE_ISO8601_DATE =
+ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?)?((Z$)|(?:[+-](?:([0-2]\d$)|([0-2]\d(?:00|30)$)|([0-2]\d:(?:00|30)$))))$/;
+const ISO8601_DATETIME = 1 as const;
+const ISO8601_MILLISECONDS = 2 as const;
+const ISO8601_TIMEZONE = 3 as const;
+const ISO8601_Z_FMT = 4 as const;
+const ISO8601_SHORT_FMT = 5 as const;
+const ISO8601_MEDIUM_FMT = 6 as const;
+const ISO8601_LONG_FMT = 7 as const;
+
+/**
+ * Helper to normalize a date value to a date object
+ * @param dateValue - Date number, string, or object
+ * @returns {date} the normalized date object
+ */
+function convertToDate(dateValue: number | string | Date): Date {
+ return dateValue instanceof Date ? dateValue : new Date(dateValue);
+}
+
+/**
+ * Converts an integer value in seconds to milliseconds.
+ * @param {number} seconds - The value in seconds
+ * @returns {number} the value in milliseconds
+ */
+function convertToMs(seconds: number): number {
+ return seconds * MILLISECONDS_PER_SECOND;
+}
+
+/**
+ * Checks whether the given date value (in unix milliseconds) is today.
+ * @param {number|string|Date} dateValue - Date object or integer representing the number of milliseconds since 1/1/1970 UTC
+ * @returns {boolean} whether the given value is today
+ */
+function isToday(dateValue: number | string | Date): boolean {
+ return new Date().toDateString() === convertToDate(dateValue).toDateString();
+}
+
+/**
+ * Checks whether the given date value (in unix milliseconds) is yesterday.
+ * @param {number|string|Date} dateValue - Date object or integer or representing the number of milliseconds since 1/1/1970 UTC
+ * @returns {boolean} whether the given value is yesterday
+ */
+function isYesterday(dateValue: number | string | Date): boolean {
+ return isToday(convertToDate(dateValue).getTime() + MILLISECONDS_PER_DAY);
+}
+
+/**
+ * Checks whether the given date value (in unix milliseconds) is tomorrow.
+ * @param {number|string|Date} dateValue - Date object or integer or representing the number of milliseconds since 1/1/1970 UTC
+ * @returns {boolean} whether the given value is tomorrow
+ */
+function isTomorrow(dateValue: number | string | Date): boolean {
+ return isToday(convertToDate(dateValue).getTime() - MILLISECONDS_PER_DAY);
+}
+
+/**
+ * Checks whether the given date value (in unix milliseconds) is in the current month.
+ * @param {number|string|Date} dateValue - Date object or integer representing the number of milliseconds since 1/1/1970 UTC
+ * @returns {boolean} whether the given value is in the current month
+ */
+function isCurrentMonth(dateValue: number | string | Date): boolean {
+ return new Date().getMonth() === convertToDate(dateValue).getMonth();
+}
+
+/**
+ * Checks whether the given date value (in unix milliseconds) is in the current year.
+ * @param {number|string|Date} dateValue - Date object or integer representing the number of milliseconds since 1/1/1970 UTC
+ * @returns {boolean} whether the given value is in the current year
+ */
+function isCurrentYear(dateValue: number | string | Date): boolean {
+ return new Date().getFullYear() === convertToDate(dateValue).getFullYear();
+}
+
+/**
+ * Formats a number of seconds as a time string
+ *
+ * @param {number} seconds - seconds
+ * @return {string} a string formatted like 3:57:35
+ */
+function formatTime(seconds: number): string {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = Math.floor((seconds % 3600) % 60);
+ const hour = h > 0 ? `${h.toString()}:` : '';
+ const sec = s < 10 ? `0${s.toString()}` : s.toString();
+ let min = m.toString();
+ if (h > 0 && m < 10) {
+ min = `0${min}`;
+ }
+ return `${hour}${min}:${sec}`;
+}
+
+/**
+ * Adds time to a given dateValue
+ *
+ * @param {number|Date} dateValue - date or integer value to add time to
+ * @param {number} timeToAdd - amount of time to add in ms
+ * @return {number|Date} the modified date or integer
+ */
+function addTime(dateValue: number | Date, timeToAdd: number): number | Date {
+ if (dateValue instanceof Date) {
+ return new Date(dateValue.getTime() + timeToAdd);
+ }
+
+ return dateValue + timeToAdd;
+}
+
+/**
+ * Will convert
+ * 2018-06-13T07:00:00.000Z
+ * to
+ * 2018-06-13T00:00:00.000Z
+ *
+ * This is the opposite of convertISOStringToUTCDate
+ *
+ * @param {Date} date
+ * @return {number}
+ */
+function convertDateToUnixMidnightTime(date: Date) {
+ // date is localized to 00:00:00 at system/browser timezone
+ const utcUnixTimeInMs = date.getTime();
+
+ // timezone an integer offset; minutes behind GMT
+ // we use the browser timezone offset instead of the user's,
+ // because the datepicker uses the browser to get the "midnight"
+ // time in the user's timezone with getTime()
+ const timezoneOffsetInMins = date.getTimezoneOffset();
+ const timezoneOffsetInMs = timezoneOffsetInMins * MILLISECONDS_PER_MINUTE;
+
+ // we need the unix/epoch time for midnight on the date selected
+ const unixDayMidnightTime = utcUnixTimeInMs - timezoneOffsetInMs;
+ return unixDayMidnightTime;
+}
+
+/**
+ * Will check to see if a date object is not valid, according to the browser
+ * JS engine.
+ *
+ * @param {Date} date
+ * @return {boolean} whether the date value passes validation
+ */
+function isValidDate(date: Date): boolean {
+ return !isNaN(date.getTime());
+}
+
+/**
+ * Will convert ISO8601-compatible dates (with zone designators)
+ * 2018-06-13T00:00:00.000-0500
+ * or
+ * 2018-06-13T00:00:00.000-05
+ *
+ * to
+ * 2018-06-13T00:00:00.000-05:00
+ *
+ * Equivalent formats between the two (e.g., uzing 'Z') will remain unchanged.
+ * If the date format cannot be converted, it will pass along the existing value
+ * @param {string} isoString
+ * @return {string} converted date format, if applicable
+ */
+function convertISOStringtoRFC3339String(isoString: string): string {
+ // test that the date format inbound is ISO8601-compatible
+ if (RE_ISO8601_DATE.test(isoString)) {
+ // if it is, parse out the timezone part if it's in a longer format
+ // use the capture groups instead of the split result for the datetime and the time zone
+ const parseDate = isoString.split(RE_ISO8601_DATE);
+ let dateTime = parseDate[ISO8601_DATETIME];
+ const milliseconds = parseDate[ISO8601_MILLISECONDS];
+ const timeZone = parseDate[ISO8601_TIMEZONE];
+
+ // add milliseconds if missing, to standardize output
+ if (!milliseconds) {
+ dateTime += '.000';
+ }
+
+ if (parseDate[ISO8601_Z_FMT]) {
+ return isoString;
+ }
+
+ if (parseDate[ISO8601_SHORT_FMT]) {
+ return `${dateTime + timeZone}:00`;
+ }
+
+ if (parseDate[ISO8601_MEDIUM_FMT]) {
+ return `${dateTime + timeZone.substr(0, 3)}:${timeZone.substr(3)}`;
+ }
+
+ if (parseDate[ISO8601_LONG_FMT]) {
+ return isoString;
+ }
+ }
+ return isoString;
+}
+
+/**
+ * Will convert
+ * 2018-06-13T00:00:00.000Z
+ * to
+ * 2018-06-13T07:00:00.000Z
+ *
+ * This is the opposite of convertDateToUnixMidnightTime
+ *
+ * @param {string} isoString - ISO string in UTC time zone
+ */
+function convertISOStringToUTCDate(isoString: string): Date {
+ // get date in UTC midnight time
+ const utcDate = new Date(convertISOStringtoRFC3339String(isoString));
+ const utcTime = utcDate.getTime();
+
+ // get browser's timezone
+ const timezoneOffsetInMins = utcDate.getTimezoneOffset();
+ const timezoneOffsetInMs = timezoneOffsetInMins * MILLISECONDS_PER_MINUTE;
+
+ // return date in utc timezone
+ const localizedUnixTimeInMs = utcTime + timezoneOffsetInMs;
+ return new Date(localizedUnixTimeInMs);
+}
+
+export {
+ convertToDate,
+ convertToMs,
+ convertDateToUnixMidnightTime,
+ convertISOStringToUTCDate,
+ convertISOStringtoRFC3339String,
+ isToday,
+ isTomorrow,
+ isValidDate,
+ isYesterday,
+ isCurrentMonth,
+ isCurrentYear,
+ formatTime,
+ addTime,
+};