From 172a50cab010308443350a6ddd09f883b06c0906 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 19 May 2022 14:48:32 +0800 Subject: [PATCH] Migrate @woocommerce/date to TS --- packages/js/date/package.json | 2 + packages/js/date/src/{index.js => index.ts} | 319 ++++++++++++++------ packages/js/date/tsconfig.json | 7 +- packages/js/date/typings/global.d.ts | 10 + pnpm-lock.yaml | 5 +- 5 files changed, 247 insertions(+), 96 deletions(-) rename packages/js/date/src/{index.js => index.ts} (75%) create mode 100644 packages/js/date/typings/global.d.ts diff --git a/packages/js/date/package.json b/packages/js/date/package.json index 1be19e085c6..775c4c60e7e 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -19,11 +19,13 @@ }, "main": "build/index.js", "module": "build-module/index.js", + "types": "build-types", "react-native": "src/index", "dependencies": { "@wordpress/date": "^4.3.1", "@wordpress/i18n": "^4.3.1", "moment": "^2.29.1", + "moment-timezone": "^0.5.34", "qs": "^6.10.3" }, "devDependencies": { diff --git a/packages/js/date/src/index.js b/packages/js/date/src/index.ts similarity index 75% rename from packages/js/date/src/index.js rename to packages/js/date/src/index.ts index 8e493107b9b..9096ed1d92f 100644 --- a/packages/js/date/src/index.js +++ b/packages/js/date/src/index.ts @@ -2,23 +2,49 @@ * External dependencies */ import moment from 'moment'; +import 'moment-timezone'; import { find, memoize } from 'lodash'; import { __ } from '@wordpress/i18n'; import { parse } from 'qs'; -export const isoDateFormat = 'YYYY-MM-DD'; +type Query = { + [ key: string ]: string | undefined; +}; +export const isoDateFormat = 'YYYY-MM-DD'; export const defaultDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss'; /** * DateValue Object * - * @typedef {Object} DateValue - Describes the date range supplied by the date picker. + * @typedef {Object} DateValue - DateValue data about the selected period. + * @property {moment.Moment} primaryStart - Primary start of the date range. + * @property {moment.Moment} primaryEnd - Primary end of the date range. + * @property {moment.Moment} secondaryStart - Secondary start of the date range. + * @property {moment.Moment} secondaryEnd - Secondary End of the date range. + */ +export type DateValue = { + primaryStart: moment.Moment; + primaryEnd: moment.Moment; + secondaryStart: moment.Moment; + secondaryEnd: moment.Moment; +}; + +/** + * DataPickerOptions Object + * + * @typedef {Object} DataPickerOptions - Describes the date range supplied by the date picker. * @property {string} label - The translated value of the period. * @property {string} range - The human readable value of a date range. * @property {moment.Moment} after - Start of the date range. * @property {moment.Moment} before - End of the date range. */ +export type DataPickerOptions = { + label: string; + range: string; + after: moment.Moment; + before: moment.Moment; +}; /** * DateParams Object @@ -29,6 +55,12 @@ export const defaultDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss'; * @param {moment.Moment|null} after - If the period supplied is "custom", this is the after date * @param {moment.Moment|null} before - If the period supplied is "custom", this is the before date */ +export type DateParams = { + period: string; + compare: string; + after: moment.Moment | null; + before: moment.Moment | null; +}; export const presetValues = [ { value: 'today', label: __( 'Today', 'woocommerce' ) }, @@ -55,6 +87,9 @@ export const periods = [ }, ]; +const isValidMomentInput = ( input: unknown ): input is moment.MomentInput => + moment( input as moment.MomentInput ).isValid(); + /** * Adds timestamp to a string date. * @@ -62,7 +97,7 @@ export const periods = [ * @param {string} timeOfDay - Either `start`, `now` or `end` of the day. * @return {string} - String date with timestamp attached. */ -export const appendTimestamp = ( date, timeOfDay ) => { +export const appendTimestamp = ( date: moment.Moment, timeOfDay: string ) => { if ( timeOfDay === 'start' ) { return date.startOf( 'day' ).format( defaultDateTimeFormat ); } @@ -84,9 +119,9 @@ export const appendTimestamp = ( date, timeOfDay ) => { * * @param {string} format - localized date string format * @param {string} str - date string - * @return {Object|null} - Moment object representing given string + * @return {moment.Moment|null} - Moment object representing given string */ -export function toMoment( format, str ) { +export function toMoment( format: string, str: string ) { if ( moment.isMoment( str ) ) { return str.isValid() ? str : null; } @@ -100,11 +135,11 @@ export function toMoment( format, str ) { /** * Given two dates, derive a string representation * - * @param {Object} after - start date - * @param {Object} before - end date + * @param {moment.Moment} after - start date + * @param {moment.Moment} before - end date * @return {string} - text value for the supplied date range */ -export function getRangeLabel( after, before ) { +export function getRangeLabel( after: moment.Moment, before: moment.Moment ) { const isSameYear = after.year() === before.year(); const isSameMonth = isSameYear && after.month() === before.month(); const isSameDay = @@ -117,7 +152,10 @@ export function getRangeLabel( after, before ) { const afterDate = after.date(); return after .format( fullDateFormat ) - .replace( afterDate, `${ afterDate } - ${ before.date() }` ); + .replace( + String( afterDate ), + `${ afterDate } - ${ before.date() }` + ); } else if ( isSameYear ) { const monthDayFormat = __( 'MMM D', 'woocommerce' ); return `${ after.format( monthDayFormat ) } - ${ before.format( @@ -149,11 +187,14 @@ export function getStoreTimeZoneMoment() { /** * Get a DateValue object for a period prior to the current period. * - * @param {string} period - the chosen period - * @param {string} compare - `previous_period` or `previous_year` + * @param {moment.DurationInputArg2} period - the chosen period + * @param {string} compare - `previous_period` or `previous_year` * @return {DateValue} - DateValue data about the selected period */ -export function getLastPeriod( period, compare ) { +export function getLastPeriod( + period: moment.DurationInputArg2, + compare: string +) { const primaryStart = getStoreTimeZoneMoment() .startOf( period ) .subtract( 1, period ); @@ -198,11 +239,14 @@ export function getLastPeriod( period, compare ) { * Get a DateValue object for a curent period. The period begins on the first day of the period, * and ends on the current day. * - * @param {string} period - the chosen period - * @param {string} compare - `previous_period` or `previous_year` + * @param {moment.DurationInputArg2} period - the chosen period + * @param {string} compare - `previous_period` or `previous_year` * @return {DateValue} - DateValue data about the selected period */ -export function getCurrentPeriod( period, compare ) { +export function getCurrentPeriod( + period: moment.DurationInputArg2, + compare: string +) { const primaryStart = getStoreTimeZoneMoment().startOf( period ); const primaryEnd = getStoreTimeZoneMoment(); @@ -233,13 +277,17 @@ export function getCurrentPeriod( period, compare ) { * Get a DateValue object for a period described by a period, compare value, and start/end * dates, for custom dates. * - * @param {string} period - the chosen period - * @param {string} compare - `previous_period` or `previous_year` - * @param {Object} [after] - after date if custom period - * @param {Object} [before] - before date if custom period + * @param {string} period - the chosen period + * @param {string} compare - `previous_period` or `previous_year` + * @param {moment.Moment|null} [after] - after date if custom period + * @param {moment.Moment|null} [before] - before date if custom period * @return {DateValue} - DateValue data about the selected period */ -const getDateValue = memoize( +const getDateValue = memoize< + ( + ...args: [ string, string, moment.Moment | null, moment.Moment | null ] + ) => DateValue | undefined +>( ( period, compare, after, before ) => { switch ( period ) { case 'today': @@ -263,6 +311,12 @@ const getDateValue = memoize( case 'last_year': return getLastPeriod( 'year', compare ); case 'custom': + if ( ! after || ! before ) { + throw Error( + 'Custom date range requires both after and before dates.' + ); + } + const difference = before.diff( after, 'days' ); if ( compare === 'previous_period' ) { const secondaryEnd = after.clone().subtract( 1, 'days' ); @@ -296,15 +350,31 @@ const getDateValue = memoize( /** * Memoized internal logic of getDateParamsFromQuery(). * - * @param {string} period - period value, ie `last_week` - * @param {string} compare - compare value, ie `previous_year` - * @param {string} after - date in iso date format, ie `2018-07-03` - * @param {string} before - date in iso date format, ie `2018-07-03` - * @param {string} defaultDateRange - the store's default date range - * @return {Object} - date parameters derived from query parameters with added defaults + * @param {string|undefined} period - period value, ie `last_week` + * @param {string|undefined} compare - compare value, ie `previous_year` + * @param {string|undefined} after - date in iso date format, ie `2018-07-03` + * @param {string|undefined} before - date in iso date format, ie `2018-07-03` + * @param {string} defaultDateRange - the store's default date range + * @return {DateParams} - date parameters derived from query parameters with added defaults */ -const getDateParamsFromQueryMemoized = memoize( - ( period, compare, after, before, defaultDateRange ) => { +const getDateParamsFromQueryMemoized = memoize< + ( + ...args: [ + string | undefined, + string | undefined, + string | undefined, + string | undefined, + string + ] + ) => DateParams +>( + ( + period: string | undefined, + compare: string | undefined, + after: string | undefined, + before: string | undefined, + defaultDateRange: string + ) => { if ( period && compare ) { return { period, @@ -317,13 +387,30 @@ const getDateParamsFromQueryMemoized = memoize( defaultDateRange.replace( /&/g, '&' ) ); + if ( typeof queryDefaults.period !== 'string' ) { + throw Error( + `Unexpected default period type ${ queryDefaults.period }` + ); + } + + if ( typeof queryDefaults.compare !== 'string' ) { + throw Error( + `Unexpected default compare type ${ queryDefaults.compare }` + ); + } + return { period: queryDefaults.period, compare: queryDefaults.compare, - after: queryDefaults.after ? moment( queryDefaults.after ) : null, - before: queryDefaults.before - ? moment( queryDefaults.before ) - : null, + after: + queryDefaults.after && isValidMomentInput( queryDefaults.after ) + ? moment( queryDefaults.after ) + : null, + before: + queryDefaults.before && + isValidMomentInput( queryDefaults.before ) + ? moment( queryDefaults.before ) + : null, }; }, ( period, compare, after, before, defaultDateRange ) => @@ -342,7 +429,7 @@ const getDateParamsFromQueryMemoized = memoize( * @return {DateParams} - date parameters derived from query parameters with added defaults */ export const getDateParamsFromQuery = ( - query, + query: Query, defaultDateRange = 'period=month&compare=previous_year' ) => { const { period, compare, after, before } = query; @@ -359,15 +446,29 @@ export const getDateParamsFromQuery = ( /** * Memoized internal logic of getCurrentDates(). * - * @param {string} period - period value, ie `last_week` - * @param {string} compare - compare value, ie `previous_year` - * @param {Object} primaryStart - primary query start DateTime, in Moment instance. - * @param {Object} primaryEnd - primary query start DateTime, in Moment instance. - * @param {Object} secondaryStart - primary query start DateTime, in Moment instance. - * @param {Object} secondaryEnd - primary query start DateTime, in Moment instance. - * @return {{primary: DateValue, secondary: DateValue}} - Primary and secondary DateValue objects + * @param {string|undefined} period - period value, ie `last_week` + * @param {string|undefined} compare - compare value, ie `previous_year` + * @param {Object} primaryStart - primary query start DateTime, in Moment instance. + * @param {Object} primaryEnd - primary query start DateTime, in Moment instance. + * @param {Object} secondaryStart - secondary query start DateTime, in Moment instance. + * @param {Object} secondaryEnd - secondary query start DateTime, in Moment instance. + * @return {{primary: DataPickerOptions, secondary: DataPickerOptions}} - Primary and secondary DataPickerOptions objects */ -const getCurrentDatesMemoized = memoize( +const getCurrentDatesMemoized = memoize< + ( + ...args: [ + string, + string, + moment.Moment, + moment.Moment, + moment.Moment, + moment.Moment + ] + ) => { + primary: DataPickerOptions; + secondary: DataPickerOptions; + } +>( ( period, compare, @@ -377,14 +478,21 @@ const getCurrentDatesMemoized = memoize( secondaryEnd ) => ( { primary: { - label: find( presetValues, ( item ) => item.value === period ) - .label, + label: ( + find( presetValues, ( item ) => item.value === period ) || { + label: '', + } + ).label, range: getRangeLabel( primaryStart, primaryEnd ), after: primaryStart, before: primaryEnd, }, secondary: { - label: find( periods, ( item ) => item.value === compare ).label, + label: ( + find( periods, ( item ) => item.value === compare ) || { + label: '', + } + ).label, range: getRangeLabel( secondaryStart, secondaryEnd ), after: secondaryStart, before: secondaryEnd, @@ -417,22 +525,29 @@ const getCurrentDatesMemoized = memoize( * @param {string} query.after - date in iso date format, ie `2018-07-03` * @param {string} query.before - date in iso date format, ie `2018-07-03` * @param {string} defaultDateRange - the store's default date range - * @return {{primary: DateValue, secondary: DateValue}} - Primary and secondary DateValue objects + * @return {{primary: DataPickerOptions, secondary: DataPickerOptions}} - Primary and secondary DataPickerOptions objects */ export const getCurrentDates = ( - query, + query: Query, defaultDateRange = 'period=month&compare=previous_year' ) => { const { period, compare, after, before } = getDateParamsFromQuery( query, defaultDateRange ); + + const dateValue = getDateValue( period, compare, after, before ); + + if ( ! dateValue ) { + throw Error( 'Invalid date range' ); + } + const { primaryStart, primaryEnd, secondaryStart, secondaryEnd, - } = getDateValue( period, compare, after, before ); + } = dateValue; return getCurrentDatesMemoized( period, @@ -448,10 +563,13 @@ export const getCurrentDates = ( * Calculates the date difference between two dates. Used in calculating a matching date for previous period. * * @param {string} date - Date to compare - * @param {string} date2 - Seconary date to compare + * @param {string} date2 - Secondary date to compare * @return {number} - Difference in days. */ -export const getDateDifferenceInDays = ( date, date2 ) => { +export const getDateDifferenceInDays = ( + date: moment.MomentInput, + date2: moment.MomentInput +) => { const _date = moment( date ); const _date2 = moment( date2 ); return _date.diff( _date2, 'days' ); @@ -460,14 +578,20 @@ export const getDateDifferenceInDays = ( date, date2 ) => { /** * Get the previous date for either the previous period of year. * - * @param {string} date - Base date - * @param {string} date1 - primary start - * @param {string} date2 - secondary start - * @param {string} compare - `previous_period` or `previous_year` - * @param {string} interval - interval + * @param {string} date - Base date + * @param {string} date1 - primary start + * @param {string} date2 - secondary start + * @param {string} compare - `previous_period` or `previous_year` + * @param {moment.unitOfTime.Diff} interval - interval * @return {Object} - Calculated date */ -export const getPreviousDate = ( date, date1, date2, compare, interval ) => { +export const getPreviousDate = ( + date: string, + date1: string, + date2: string, + compare: string, + interval: moment.unitOfTime.Diff | moment.DurationInputArg2 +) => { const dateMoment = moment( date ); if ( compare === 'previous_year' ) { @@ -484,12 +608,12 @@ export const getPreviousDate = ( date, date1, date2, compare, interval ) => { /** * Returns the allowed selectable intervals for a specific query. * - * @param {Object} query Current query + * @param {Query} query Current query * @param {string} defaultDateRange - the store's default date range * @return {Array} Array containing allowed intervals. */ export function getAllowedIntervalsForQuery( - query, + query: Query, defaultDateRange = 'period=&compare=previous_year' ) { const { period } = getDateParamsFromQuery( query, defaultDateRange ); @@ -546,12 +670,12 @@ export function getAllowedIntervalsForQuery( /** * Returns the current interval to use. * - * @param {Object} query Current query + * @param {Query} query Current query * @param {string} defaultDateRange - the store's default date range * @return {string} Current interval. */ export function getIntervalForQuery( - query, + query: Query, defaultDateRange = 'period=&compare=previous_year' ) { const allowed = getAllowedIntervalsForQuery( query, defaultDateRange ); @@ -567,11 +691,11 @@ export function getIntervalForQuery( /** * Returns the current chart type to use. * - * @param {Object} query Current query + * @param {Query} query Current query * @param {string} query.chartType * @return {string} Current chart type. */ -export function getChartTypeForQuery( { chartType } ) { +export function getChartTypeForQuery( { chartType = '' }: Query ) { if ( [ 'line', 'bar' ].includes( chartType ) ) { return chartType; } @@ -582,30 +706,6 @@ export const dayTicksThreshold = 63; export const weekTicksThreshold = 9; export const defaultTableDateFormat = 'm/d/Y'; -/** - * Returns date formats for the current interval. - * - * @param {string} interval Interval to get date formats for. - * @param {number} [ticks] Number of ticks the axis will have. - * @param {Object} [option] Options - * @param {string} [option.type] Date format type, d3 or php, defaults to d3. - * @return {string} Current interval. - */ -export function getDateFormatsForInterval( - interval, - ticks = 0, - option = { type: 'd3' } -) { - switch ( option.type ) { - case 'php': - return getDateFormatsForIntervalPhp( interval, ticks ); - - case 'd3': - default: - return getDateFormatsForIntervalD3( interval, ticks ); - } -} - /** * Returns d3 date formats for the current interval. * See https://github.com/d3/d3-time-format for chart formats. @@ -614,7 +714,7 @@ export function getDateFormatsForInterval( * @param {number} [ticks] Number of ticks the axis will have. * @return {string} Current interval. */ -export function getDateFormatsForIntervalD3( interval, ticks = 0 ) { +export function getDateFormatsForIntervalD3( interval: string, ticks = 0 ) { let screenReaderFormat = '%B %-d, %Y'; let tooltipLabelFormat = '%B %-d, %Y'; let xFormat = '%Y-%m-%d'; @@ -681,7 +781,7 @@ export function getDateFormatsForIntervalD3( interval, ticks = 0 ) { * @param {number} [ticks] Number of ticks the axis will have. * @return {string} Current interval. */ -export function getDateFormatsForIntervalPhp( interval, ticks = 0 ) { +export function getDateFormatsForIntervalPhp( interval: string, ticks = 0 ) { let screenReaderFormat = 'F j, Y'; let tooltipLabelFormat = 'F j, Y'; let xFormat = 'Y-m-d'; @@ -745,6 +845,30 @@ export function getDateFormatsForIntervalPhp( interval, ticks = 0 ) { }; } +/** + * Returns date formats for the current interval. + * + * @param {string} interval Interval to get date formats for. + * @param {number} [ticks] Number of ticks the axis will have. + * @param {Object} [option] Options + * @param {string} [option.type] Date format type, d3 or php, defaults to d3. + * @return {string} Current interval. + */ +export function getDateFormatsForInterval( + interval: string, + ticks = 0, + option = { type: 'd3' } +) { + switch ( option.type ) { + case 'php': + return getDateFormatsForIntervalPhp( interval, ticks ); + + case 'd3': + default: + return getDateFormatsForIntervalD3( interval, ticks ); + } +} + /** * Gutenberg's moment instance is loaded with i18n values, which are * PHP date formats, ie 'LLL: "F j, Y g:i a"'. Override those with translations @@ -754,7 +878,13 @@ export function getDateFormatsForIntervalPhp( interval, ticks = 0 ) { * @param {string} config.userLocale * @param {Array} config.weekdaysShort */ -export function loadLocaleData( { userLocale, weekdaysShort } ) { +export function loadLocaleData( { + userLocale, + weekdaysShort, +}: { + userLocale: string; + weekdaysShort: moment.LocaleSpecification[ 'weekdaysMin' ]; +} ) { // Don't update if the wp locale hasn't been set yet, like in unit tests, for instance. if ( moment.locale() !== 'en' ) { moment.updateLocale( userLocale, { @@ -764,6 +894,9 @@ export function loadLocaleData( { userLocale, weekdaysShort } ) { LLL: __( 'D MMMM YYYY LT', 'woocommerce' ), LLLL: __( 'dddd, D MMMM YYYY LT', 'woocommerce' ), LT: __( 'HH:mm', 'woocommerce' ), + // Set LTS to default LTS locale format because we don't have a specific format for it. + // Reference https://github.com/moment/moment/blob/develop/dist/moment.js + LTS: 'h:mm:ss A', }, weekdaysMin: weekdaysShort, } ); @@ -794,11 +927,11 @@ export const dateValidationMessages = { * @return {Object} validatedDate - validated date object */ export function validateDateInputForRange( - type, - value, - before, - after, - format + type: string, + value: string, + before: moment.MomentInput | null, + after: moment.MomentInput | null, + format: string ) { const date = toMoment( format, value ); if ( ! date ) { diff --git a/packages/js/date/tsconfig.json b/packages/js/date/tsconfig.json index e8f14a25fa4..ea9f201d401 100644 --- a/packages/js/date/tsconfig.json +++ b/packages/js/date/tsconfig.json @@ -2,6 +2,9 @@ "extends": "../tsconfig", "compilerOptions": { "rootDir": "src", - "outDir": "build-module" + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types" } -} \ No newline at end of file +} diff --git a/packages/js/date/typings/global.d.ts b/packages/js/date/typings/global.d.ts new file mode 100644 index 00000000000..d22756ee2e3 --- /dev/null +++ b/packages/js/date/typings/global.d.ts @@ -0,0 +1,10 @@ +declare global { + interface Window { + wcSettings: { + timeZone?: string; + }; + } +} + +/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */ +export {}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56dcab3b1cf..c904d574273 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,6 +560,7 @@ importers: jest: ^27.5.1 jest-cli: ^27.5.1 moment: ^2.29.1 + moment-timezone: ^0.5.34 qs: ^6.10.3 rimraf: ^3.0.2 ts-jest: ^27.1.3 @@ -568,6 +569,7 @@ importers: '@wordpress/date': 4.4.1 '@wordpress/i18n': 4.4.1 moment: 2.29.1 + moment-timezone: 0.5.34 qs: 6.10.3 devDependencies: '@babel/core': 7.17.8 @@ -13713,6 +13715,7 @@ packages: re-resizable: 4.11.0 transitivePeerDependencies: - react + - react-dom dev: true /@types/wordpress__compose/4.0.1: @@ -18579,7 +18582,7 @@ packages: dev: true /buffer-crc32/0.2.13: - resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} /buffer-fill/1.0.0: resolution: {integrity: sha1-+PeLdniYiO858gXNY39o5wISKyw=}