Skip to content

Commit a37c2cc

Browse files
authored
new(DateTime, DateTimeRange, DateTimeSelect, Price, PriceComparison, PriceGroup): Add empty fallback if an invalid value is provided. (#366)
1 parent 06fc78c commit a37c2cc

File tree

21 files changed

+349
-67
lines changed

21 files changed

+349
-67
lines changed

packages/core/src/components/DatePicker/story.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class DatePickerResetDemo extends React.Component<{}, ResetState> {
7474
showResetButton
7575
initialMonth={new Date(2019, 1, 1)}
7676
selectedDays={selectedDays}
77+
// @ts-ignore valid `at` won't return `null`
7778
todayButton={DateTime.format({
7879
at: Date.now(),
7980
medium: true,
@@ -174,6 +175,7 @@ class DatePickerMouseRangeSelectDemo extends React.Component<{}, RangeState> {
174175
modifiers={modifiers}
175176
numberOfMonths={2}
176177
selectedDays={selectedDays}
178+
// @ts-ignore valid `at` won't return `null`
177179
todayButton={DateTime.format({
178180
at: Date.now(),
179181
medium: true,
@@ -209,6 +211,7 @@ export function displayATodayButton() {
209211
return (
210212
<DatePicker
211213
initialMonth={new Date(2019, 1, 1)}
214+
// @ts-ignore valid `at` won't return `null`
212215
todayButton={DateTime.format({
213216
at: Date.now(),
214217
medium: true,

packages/core/src/components/DatePickerInput/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default class DatePickerInput extends React.Component<
7171
// Update the parent form with the selected value.
7272
// We also don't have a real event object, so fake it.
7373
this.props.onChange(
74-
this.formatDate(day),
74+
this.formatDate(day)!,
7575
day,
7676
// @ts-ignore
7777
{},
@@ -84,6 +84,7 @@ export default class DatePickerInput extends React.Component<
8484

8585
parseDate = (value: string, format?: string, locale?: string) => {
8686
try {
87+
// @ts-ignore Allow it to fail
8788
return createDateTime(value, {
8889
sourceFormat: format ?? this.getFormat(),
8990
locale: locale ?? this.props.locale,
@@ -103,7 +104,7 @@ export default class DatePickerInput extends React.Component<
103104
locale: locale ?? this.props.locale,
104105
noTime: true,
105106
noTimezone: true,
106-
});
107+
})!;
107108
};
108109

109110
render() {

packages/core/src/components/DateTime/index.tsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default class DateTime extends React.PureComponent<DateTimeProps> {
9797
withDay: false,
9898
};
9999

100-
static format(props: DateTimeProps): string {
100+
static format(props: DateTimeProps): string | null {
101101
const {
102102
at,
103103
clock,
@@ -128,21 +128,24 @@ export default class DateTime extends React.PureComponent<DateTimeProps> {
128128
sourceFormat,
129129
timezone,
130130
});
131-
let format = baseFormat || '';
132-
let affixDay = withDay;
133-
let affixTime = true;
134131

135-
if (__DEV__) {
136-
if (!timeStamp.isValid) {
137-
throw new Error('Invalid timestamp passed to `DateTime`.');
132+
if (!timeStamp || (timeStamp && !timeStamp.isValid)) {
133+
if (__DEV__) {
134+
console.error('Invalid timestamp passed to `DateTime`.');
138135
}
136+
137+
return null;
139138
}
140139

140+
let format = baseFormat || '';
141+
let affixDay = withDay;
142+
let affixTime = true;
143+
141144
// Disable future dates
142145
if (noFuture) {
143146
const now = createDateTime(null, { timezone });
144147

145-
if (timeStamp > now) {
148+
if (now && timeStamp > now) {
146149
timeStamp = now;
147150
}
148151
}
@@ -187,8 +190,13 @@ export default class DateTime extends React.PureComponent<DateTimeProps> {
187190
return timeStamp.toFormat(format);
188191
}
189192

190-
static relative(timeStamp: DateTimeType, options: ToRelativeOptions = {}): string {
193+
static relative(timeStamp: DateTimeType, options: ToRelativeOptions = {}): string | null {
191194
const relative = createDateTime(timeStamp);
195+
196+
if (!relative) {
197+
return null;
198+
}
199+
192200
const diff = DateTime.diff(relative, options.base);
193201
const fewPhrase =
194202
options.style === 'narrow'
@@ -215,15 +223,21 @@ export default class DateTime extends React.PureComponent<DateTimeProps> {
215223
}
216224

217225
static diff(to: DateTimeType, from: DateTimeType | null = null): number {
218-
return (
219-
createDateTime(to, { timezone: 'UTC' }).toMillis() -
220-
createDateTime(from, { timezone: 'UTC' }).toMillis()
221-
);
226+
const toDate = createDateTime(to, { timezone: 'UTC' });
227+
const fromDate = createDateTime(from, { timezone: 'UTC' });
228+
229+
if (toDate && fromDate) {
230+
return toDate.toMillis() - fromDate.toMillis();
231+
}
232+
233+
return 0;
222234
}
223235

224236
getRefreshInterval() {
225237
const { at, sourceFormat } = this.props;
226-
const difference = Math.abs(DateTime.diff(createDateTime(at, { sourceFormat })));
238+
const difference = Math.abs(
239+
DateTime.diff(createDateTime(at, { sourceFormat }) ?? MAX_RELATIVE_DATETIME_REFRESH_INTERVAL),
240+
);
227241

228242
// Decay refresh rate based on how long its been since the given timestamp
229243
// < 1 minute: update every 5 seconds
@@ -239,8 +253,9 @@ export default class DateTime extends React.PureComponent<DateTimeProps> {
239253

240254
rfc() {
241255
const { at, sourceFormat } = this.props;
256+
const date = createDateTime(at, { sourceFormat });
242257

243-
return createDateTime(at, { sourceFormat }).toFormat("yyyy-MM-dd'T'HH:mm:ssZZ"); // RFC3339
258+
return date ? date.toFormat("yyyy-MM-dd'T'HH:mm:ssZZ") : ''; // RFC3339
244259
}
245260

246261
renderTimeElement = () => {

packages/core/src/components/DateTime/story.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import DateTime from '.';
3+
import Empty from '../Empty';
34

45
const future = new Date();
56
future.setDate(future.getDate() + 12);
@@ -77,3 +78,24 @@ export function usingStaticMethod() {
7778
usingStaticMethod.story = {
7879
name: 'Using static method.',
7980
};
81+
82+
export function withAnInvalidAtValue() {
83+
return (
84+
<div>
85+
<div>
86+
Component fallback: <DateTime at="[Hidden]" />
87+
</div>
88+
89+
<div>Static method with fallback: {DateTime.format({ at: '[Hidden]', long: true })}</div>
90+
91+
<div>
92+
Static method with custom fallback:{' '}
93+
{DateTime.format({ at: '[Hidden]', long: true }) || <Empty />}
94+
</div>
95+
</div>
96+
);
97+
}
98+
99+
withAnInvalidAtValue.story = {
100+
name: 'Fallback when an invalid date value is provided.',
101+
};

packages/core/src/components/DateTimeRange/index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,24 @@ export default function DateTimeRange({
3535
const fromTimeStamp = createDateTime(from, { locale, timezone });
3636
const toTimeStamp = createDateTime(to, { locale, timezone });
3737

38-
if (__DEV__) {
39-
if (!fromTimeStamp.isValid || !toTimeStamp.isValid) {
40-
throw new Error('Invalid timestamps passed to `DateTimeRange`.');
38+
if (
39+
!fromTimeStamp ||
40+
!toTimeStamp ||
41+
(fromTimeStamp && !fromTimeStamp.isValid) ||
42+
(toTimeStamp && !toTimeStamp.isValid)
43+
) {
44+
if (__DEV__) {
45+
console.error(
46+
`Invalid ${!fromTimeStamp ? 'fromTimeStamp' : 'toTimeStamp'} passed to \`DateTimeRange\`.`,
47+
);
4148
}
4249

50+
return <Empty />;
51+
}
52+
53+
if (__DEV__) {
4354
if (toTimeStamp < fromTimeStamp) {
44-
throw new Error('Invalid chronological order of timestamps passed to `DateTimeRange`.');
55+
console.error('Invalid chronological order of timestamps passed to `DateTimeRange`.');
4556
}
4657
}
4758

packages/core/src/components/DateTimeRange/story.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,25 @@ export function differentYearsRangeWithCustomSeparator() {
4747
differentYearsRangeWithCustomSeparator.story = {
4848
name: 'Different years range with custom separator.',
4949
};
50+
51+
export function withAnInvalidValues() {
52+
return (
53+
<div>
54+
<div>
55+
Invalid from: <DateTimeRange from="[Hidden]" to={new Date(2019, 1, 17, 0, 0, 0)} />
56+
</div>
57+
58+
<div>
59+
Invalid to: <DateTimeRange from={new Date(2019, 1, 15, 0, 0, 0)} to="[Hidden]" />
60+
</div>
61+
62+
<div>
63+
Both invalid: <DateTimeRange from="[Hidden]" to="[Hidden]" />
64+
</div>
65+
</div>
66+
);
67+
}
68+
69+
withAnInvalidValues.story = {
70+
name: 'Fallback when an invalid date values are provided.',
71+
};

packages/core/src/components/DateTimeSelect/index.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,15 @@ export class DateTimeSelect extends React.Component<
6666
yearPastBuffer: 80,
6767
};
6868

69-
private date = createDateTime(this.props.value, {
70-
locale: this.props.locale,
71-
timezone: this.props.timezone,
72-
}).set({ minute: 0, second: 0 });
69+
private date: DateTime = (
70+
createDateTime(this.props.value, {
71+
locale: this.props.locale,
72+
timezone: this.props.timezone,
73+
}) ?? this.getNowDate()
74+
).set({
75+
minute: 0,
76+
second: 0,
77+
});
7378

7479
state = {
7580
id: uuid(),
@@ -83,18 +88,30 @@ export class DateTimeSelect extends React.Component<
8388
// Don't set minute/second to 0 here, because when used in conjunction with the form kit,
8489
// the value is always passed down, causing the numbers to always reset to 0.
8590
if (value !== prevProps.value) {
86-
const date = createDateTime(value, {
87-
locale,
88-
timezone,
89-
});
91+
const date =
92+
createDateTime(value, {
93+
locale,
94+
timezone,
95+
}) ?? this.getNowDate();
9096

9197
this.setState({
9298
date,
93-
meridiem: date.get('hour') <= 11 ? 'am' : 'pm',
99+
meridiem: date?.get('hour') <= 11 ? 'am' : 'pm',
94100
});
95101
}
96102
}
97103

104+
getNowDate() {
105+
const { locale, timezone } = this.props;
106+
let date = DateTime.utc().setLocale(locale!);
107+
108+
if (timezone && timezone !== 'UTC') {
109+
date = date.setZone(timezone as string);
110+
}
111+
112+
return date;
113+
}
114+
98115
getDayRange(): Range {
99116
return createRange(1, this.state.date.daysInMonth).map((day) => ({
100117
label: day,
@@ -140,7 +157,7 @@ export class DateTimeSelect extends React.Component<
140157
}
141158

142159
getYearRange(): Range {
143-
const now = createDateTime();
160+
const now = createDateTime() ?? this.getNowDate();
144161

145162
return createRange(
146163
now.year - this.props.yearPastBuffer!,

packages/core/src/components/DateTimeSelect/story.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,18 @@ export function withInlineLabel() {
169169
withInlineLabel.story = {
170170
name: 'With inline label.',
171171
};
172+
173+
export function withInvalidDate() {
174+
return (
175+
<DateTimeSelect
176+
name="dts-basic"
177+
label="Label"
178+
value="[Hidden]"
179+
onChange={() => console.log('onChange')}
180+
/>
181+
);
182+
}
183+
184+
withInvalidDate.story = {
185+
name: "Fallback to today's date if value is invalid",
186+
};

packages/core/src/components/Price/index.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type CommonProps = {
2323

2424
export type PriceProps = CommonProps & {
2525
/** The amount as a number. */
26-
amount?: Amount | number | null;
26+
amount?: Amount | number | null | string;
2727
/** Native currency of the amount. */
2828
currency?: Currency;
2929
};
@@ -43,14 +43,20 @@ function Price({
4343
let currency = baseCurrency;
4444
let micros = baseMicros;
4545

46-
if (baseAmount === undefined || baseAmount === null) {
46+
if (
47+
baseAmount === undefined ||
48+
baseAmount === null ||
49+
(typeof baseAmount === 'string' && isNaN(Number(baseAmount)))
50+
) {
4751
return <Empty />;
4852
}
4953

5054
if (typeof baseAmount === 'object') {
5155
currency = baseAmount.currency;
5256
micros = baseAmount.is_micros_accuracy;
5357
amount = micros ? baseAmount.amount_micros : baseAmount.amount;
58+
} else if (typeof baseAmount === 'string') {
59+
amount = Number(baseAmount);
5460
} else if (typeof baseAmount === 'number') {
5561
amount = baseAmount;
5662
}
@@ -73,7 +79,7 @@ function Price({
7379
}
7480

7581
Price.propTypes = {
76-
amount: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
82+
amount: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.string]),
7783
display: PropTypes.oneOf(['symbol', 'code', 'name']),
7884
};
7985

packages/core/src/components/Price/story.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ export function roundedAmountInJpy() {
3131
roundedAmountInJpy.story = {
3232
name: 'Rounded amount in JPY.',
3333
};
34+
35+
export function withAnInvalidAmount() {
36+
// @ts-ignore invalid amount type on purprose to demonstrate the fallback
37+
return <Price amount="[Hidden]" currency="USD" />;
38+
}
39+
40+
withAnInvalidAmount.story = {
41+
name: 'Fallback when an invalid amount is provided.',
42+
};

0 commit comments

Comments
 (0)