Skip to content

Commit 90c5f1a

Browse files
fix: migrate from react-infinite-calendar to react-calendar (#1650)
* #migrate from react-infinite-calendar to react-calendar * #Updating yarn.lock and package.json * #fixing lint issues * #Fixing date formats from local to UTC, write helper functions, linting fixed * #addressing review - config files linting not required, ignoring * add date-fns-tz to avoid regex iso date parsing logic * simplify date logic * run prettier --------- Co-authored-by: Bill Glesias <[email protected]>
1 parent bf68e7e commit 90c5f1a

File tree

7 files changed

+242
-240
lines changed

7 files changed

+242
-240
lines changed

cypress/support/commands.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
///<reference path="../global.d.ts" />
33

44
import { pick } from "lodash/fp";
5-
import { format as formatDate } from "date-fns";
5+
import { differenceInMonths, parse as parseDate } from "date-fns";
66
import { isMobile } from "./utils";
77

88
// Import Cypress Percy plugin command (https://docs.percy.io/docs/cypress)
@@ -279,7 +279,30 @@ Cypress.Commands.add("pickDateRange", (startDate, endDate) => {
279279
});
280280

281281
const selectDate = (date: Date) => {
282-
return cy.get(`[data-date='${formatDate(date, "yyyy-MM-dd")}']`).click({ force: true });
282+
const targetDay = date.getDate();
283+
284+
return cy
285+
.get(".react-calendar__navigation__label")
286+
.invoke("text")
287+
.then((label: string) => {
288+
const parsedDate = parseDate(label, "MMMM yyyy", new Date());
289+
const monthsDiff = differenceInMonths(new Date(), parsedDate);
290+
291+
if (monthsDiff < 0) {
292+
for (let i = 0; i < Math.abs(monthsDiff); i++) {
293+
cy.get(".react-calendar__navigation__prev-button").click();
294+
}
295+
} else if (monthsDiff > 0) {
296+
for (let i = 0; i < monthsDiff; i++) {
297+
cy.get(".react-calendar__navigation__next-button").click();
298+
}
299+
}
300+
})
301+
.then(() => {
302+
cy.get(".react-calendar__month-view__days__day")
303+
.contains(new RegExp(`^${targetDay}$`))
304+
.click({ force: true });
305+
});
283306
};
284307

285308
log.snapshot("before");
@@ -289,7 +312,7 @@ Cypress.Commands.add("pickDateRange", (startDate, endDate) => {
289312

290313
// Open date range picker
291314
cy.getBySelLike("filter-date-range-button").click({ force: true });
292-
cy.get(".Cal__Header__root").should("be.visible");
315+
cy.get(".react-calendar").should("be.visible");
293316

294317
// Select date range
295318
selectDate(startDate);
@@ -298,7 +321,7 @@ Cypress.Commands.add("pickDateRange", (startDate, endDate) => {
298321
log.end();
299322
});
300323

301-
cy.get(".Cal__Header__root").should("not.exist");
324+
cy.get(".react-calendar").should("not.exist");
302325
});
303326

304327
Cypress.Commands.add("database", (operation, entity, query, logTask = false) => {

cypress/tests/ui/transaction-feeds.spec.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import {
88
TransactionStatus,
99
} from "../../../src/models";
1010
import { addDays, isWithinInterval, startOfDay } from "date-fns";
11-
import { startOfDayUTC, endOfDayUTC } from "../../../src/utils/transactionUtils";
11+
import {
12+
endOfDayUTC,
13+
isoStringToLocalMidnightStart,
14+
isoStringToLocalMidnightEnd,
15+
isoStringToLocalDateFull,
16+
localDateToIsoString,
17+
} from "../../../src/utils/transactionUtils";
1218
import { isMobile } from "../../support/utils";
1319

1420
const { _ } = Cypress;
@@ -215,19 +221,21 @@ describe("Transaction Feed", function () {
215221
if (isMobile()) {
216222
it("closes date range picker modal", () => {
217223
cy.getBySelLike("filter-date-range-button").click({ force: true });
218-
cy.get(".Cal__Header__root").should("be.visible");
224+
cy.get(".react-calendar").should("be.visible");
219225
cy.visualSnapshot("Mobile Open Date Range Picker");
220226
cy.getBySel("date-range-filter-drawer-close").click();
221-
cy.get(".Cal__Header__root").should("not.exist");
227+
cy.get(".react-calendar").should("not.exist");
222228
cy.visualSnapshot("Mobile Close Date Range Picker");
223229
});
224230
}
225231

226232
_.each(feedViews, (feed, feedName) => {
227233
it(`filters ${feedName} transaction feed by date range`, function () {
228234
cy.database("find", "transactions").then((transaction: Transaction) => {
229-
const dateRangeStart = startOfDay(new Date(transaction.createdAt));
230-
const dateRangeEnd = endOfDayUTC(addDays(dateRangeStart, 1));
235+
const dateRangeStart = isoStringToLocalMidnightStart(`${transaction.createdAt}`);
236+
const dateRangeEnd = isoStringToLocalMidnightEnd(
237+
addDays(startOfDay(transaction.createdAt), 1).toISOString()
238+
);
231239

232240
cy.getBySelLike(feed.tab).click();
233241
cy.getBySelLike(feed.tab).should("have.class", "Mui-selected");
@@ -242,16 +250,16 @@ describe("Transaction Feed", function () {
242250
cy.getBySelLike("transaction-item").should("have.length", transactions.length);
243251

244252
transactions.forEach(({ createdAt }) => {
245-
const createdAtDate = startOfDayUTC(new Date(createdAt));
253+
const createdAtDate = isoStringToLocalDateFull(`${createdAt}`);
246254

247255
expect(
248256
isWithinInterval(createdAtDate, {
249-
start: startOfDayUTC(dateRangeStart),
257+
start: dateRangeStart,
250258
end: dateRangeEnd,
251259
}),
252-
`transaction created date (${createdAtDate.toISOString()})
253-
is within ${dateRangeStart.toISOString()}
254-
and ${dateRangeEnd.toISOString()}`
260+
`transaction created date (${localDateToIsoString(createdAtDate)})
261+
is within ${localDateToIsoString(dateRangeStart)}
262+
and ${localDateToIsoString(dateRangeEnd)}`
255263
).to.equal(true);
256264
});
257265

@@ -274,7 +282,7 @@ describe("Transaction Feed", function () {
274282
});
275283

276284
it(`does not show ${feedName} transactions for out of range date limits`, function () {
277-
const dateRangeStart = startOfDay(new Date(2014, 1, 1));
285+
const dateRangeStart = startOfDay(new Date(2025, 7, 1));
278286
const dateRangeEnd = endOfDayUTC(addDays(dateRangeStart, 1));
279287

280288
cy.getBySelLike(feed.tab).click();

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@
3333
"axios": "0.28.1",
3434
"clsx": "1.2.1",
3535
"date-fns": "4.1.0",
36+
"date-fns-tz": "^3.2.0",
3637
"detect-port": "^1.5.1",
3738
"dinero.js": "1.9.1",
3839
"formik": "2.4.6",
3940
"history": "4.10.1",
4041
"postinstall-postinstall": "^2.1.0",
4142
"react": "18.2.0",
43+
"react-calendar": "^6.0.0",
4244
"react-dom": "18.2.0",
43-
"react-infinite-calendar": "2.3.1",
4445
"react-number-format": "4.9.4",
4546
"react-router": "5.3.4",
4647
"react-router-dom": "5.3.4",
@@ -82,7 +83,6 @@
8283
"@types/passport": "1.0.16",
8384
"@types/react": "^18.2.14",
8485
"@types/react-dom": "^18.2.6",
85-
"@types/react-infinite-calendar": "2.3.6",
8686
"@types/react-router": "5.1.18",
8787
"@types/react-router-dom": "5.3.3",
8888
"@types/react-virtualized": "9.21.21",
Lines changed: 76 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import { styled } from "@mui/material/styles";
33
import { format as formatDate } from "date-fns";
44
import { Popover, Chip, useTheme, Drawer, Button, useMediaQuery, colors } from "@mui/material";
55
import { ArrowDropDown as ArrowDropDownIcon, Cancel as CancelIcon } from "@mui/icons-material";
6-
import InfiniteCalendar, { Calendar, withRange } from "react-infinite-calendar";
6+
import Calendar from "react-calendar";
77

8-
import "react-infinite-calendar/styles.css";
9-
import { TransactionDateRangePayload } from "../models";
10-
import { hasDateQueryFields } from "../utils/transactionUtils";
8+
import "react-calendar/dist/Calendar.css";
9+
import { TransactionDateRangePayload, Value, ValuePiece } from "../models";
10+
import { hasDateQueryFields, localDateToUTCISOString } from "../utils/transactionUtils";
1111

1212
const PREFIX = "TransactionListDateRangeFilter";
1313

@@ -27,7 +27,6 @@ const Root = styled("div")(({ theme }) => ({
2727
}));
2828

2929
const { indigo } = colors;
30-
const CalendarWithRange = withRange(Calendar);
3130

3231
export type TransactionListDateRangeFilterProps = {
3332
filterDateRange: Function;
@@ -43,15 +42,19 @@ const TransactionListDateRangeFilter: React.FC<TransactionListDateRangeFilterPro
4342
const theme = useTheme();
4443
const xsBreakpoint = useMediaQuery(theme.breakpoints.only("xs"));
4544
const queryHasDateFields = dateRangeFilters && hasDateQueryFields(dateRangeFilters);
45+
const [calendarValue, setCalendarValue] = useState<Value>(null);
4646

4747
const [dateRangeAnchorEl, setDateRangeAnchorEl] = React.useState<HTMLDivElement | null>(null);
4848

49-
const onCalendarSelect = (e: { eventType: number; start: any; end: any }) => {
50-
if (e.eventType === 3) {
51-
filterDateRange({
52-
dateRangeStart: new Date(e.start.setUTCHours(0, 0, 0, 0)).toISOString(),
53-
dateRangeEnd: new Date(e.end.setUTCHours(23, 59, 59, 999)).toISOString(),
54-
});
49+
const onCalendarSelect = (val: Value) => {
50+
if (val && !(val instanceof Date)) {
51+
const [rangeStart, rangeEnd] = val;
52+
const calValue = {
53+
dateRangeStart: localDateToUTCISOString(rangeStart),
54+
dateRangeEnd: localDateToUTCISOString(rangeEnd),
55+
};
56+
setCalendarValue(val);
57+
filterDateRange(calValue);
5558
setDateRangeAnchorEl(null);
5659
}
5760
};
@@ -67,13 +70,16 @@ const TransactionListDateRangeFilter: React.FC<TransactionListDateRangeFilterPro
6770
const dateRangeOpen = Boolean(dateRangeAnchorEl);
6871
const dateRangeId = dateRangeOpen ? "date-range-popover" : undefined;
6972

70-
const formatButtonDate = (date: string) => {
71-
return formatDate(new Date(date), "MMM, d yyyy");
73+
const formatButtonDate = (date: Date) => {
74+
return formatDate(date, "MMM, d yyyy");
7275
};
7376

74-
const dateRangeLabel = (dateRangeFields: TransactionDateRangePayload) => {
75-
const { dateRangeStart, dateRangeEnd } = dateRangeFields;
76-
return `${formatButtonDate(dateRangeStart!)} - ${formatButtonDate(dateRangeEnd!)}`;
77+
const dateRangeLabel = (dateRangeFields: Value) => {
78+
if (dateRangeFields && !(dateRangeFields instanceof Date)) {
79+
const [dateRangeStart, dateRangeEnd] = dateRangeFields;
80+
const label = `${formatButtonDate(dateRangeStart!)} - ${formatButtonDate(dateRangeEnd!)}`;
81+
return label;
82+
}
7783
};
7884

7985
return (
@@ -95,9 +101,10 @@ const TransactionListDateRangeFilter: React.FC<TransactionListDateRangeFilterPro
95101
variant="outlined"
96102
onClick={handleDateRangeClick}
97103
data-test="transaction-list-filter-date-range-button"
98-
label={`Date: ${dateRangeLabel(dateRangeFilters)}`}
104+
label={`Date: ${dateRangeLabel(calendarValue)}`}
99105
deleteIcon={<CancelIcon data-test="transaction-list-filter-date-clear-button" />}
100106
onDelete={() => {
107+
setCalendarValue(null);
101108
resetDateRange();
102109
}}
103110
/>
@@ -118,28 +125,12 @@ const TransactionListDateRangeFilter: React.FC<TransactionListDateRangeFilterPro
118125
}}
119126
className={classes.popover}
120127
>
121-
<InfiniteCalendar
122-
data-test="transaction-list-filter-date-range"
123-
width={xsBreakpoint ? window.innerWidth : 350}
124-
height={xsBreakpoint ? window.innerHeight : 300}
125-
rowHeight={50}
126-
Component={CalendarWithRange}
127-
selected={false}
128-
onSelect={onCalendarSelect}
129-
locale={{
130-
headerFormat: "MMM Do",
131-
}}
132-
theme={{
133-
accentColor: indigo["400"],
134-
headerColor: indigo["500"],
135-
weekdayColor: indigo["300"],
136-
selectionColor: indigo["300"],
137-
floatingNav: {
138-
background: indigo["400"],
139-
color: "#FFF",
140-
chevron: "#FFA726",
141-
},
142-
}}
128+
<RangeCalendar
129+
onCalendarSelect={onCalendarSelect}
130+
xsBreakpoint={xsBreakpoint}
131+
color={indigo}
132+
dataTest="transaction-list-filter-date-range"
133+
defaultValue={calendarValue}
143134
/>
144135
</Popover>
145136
)}
@@ -154,33 +145,56 @@ const TransactionListDateRangeFilter: React.FC<TransactionListDateRangeFilterPro
154145
<Button data-test="date-range-filter-drawer-close" onClick={() => handleDateRangeClose()}>
155146
Close
156147
</Button>
157-
<InfiniteCalendar
158-
data-test="transaction-list-filter-date-range"
159-
width={window.innerWidth}
160-
height={window.innerHeight - 185}
161-
rowHeight={50}
162-
Component={CalendarWithRange}
163-
selected={false}
164-
onSelect={onCalendarSelect}
165-
locale={{
166-
headerFormat: "MMM Do",
167-
}}
168-
theme={{
169-
accentColor: indigo["400"],
170-
headerColor: indigo["500"],
171-
weekdayColor: indigo["300"],
172-
selectionColor: indigo["300"],
173-
floatingNav: {
174-
background: indigo["400"],
175-
color: "#FFF",
176-
chevron: "#FFA726",
177-
},
178-
}}
148+
<RangeCalendar
149+
onCalendarSelect={onCalendarSelect}
150+
xsBreakpoint={xsBreakpoint}
151+
color={indigo}
152+
dataTest="transaction-list-filter-date-range"
153+
defaultValue={calendarValue}
179154
/>
180155
</Drawer>
181156
)}
182157
</Root>
183158
);
184159
};
185160

161+
export function RangeCalendar({
162+
onCalendarSelect,
163+
xsBreakpoint,
164+
color,
165+
dataTest,
166+
defaultValue,
167+
}: {
168+
onCalendarSelect: (value: Value) => void;
169+
xsBreakpoint: boolean;
170+
color: Record<string, string>;
171+
dataTest: string;
172+
defaultValue: Value;
173+
}) {
174+
const [value, setValue] = useState<Value>(defaultValue);
175+
176+
const width = xsBreakpoint ? window.innerWidth : 350;
177+
const height = xsBreakpoint ? window.innerHeight : 300;
178+
179+
const handleChange = (val: Value, _: any) => {
180+
setValue(val);
181+
onCalendarSelect(val);
182+
};
183+
184+
return (
185+
<div
186+
data-test={dataTest}
187+
style={{
188+
width,
189+
maxWidth: "100%",
190+
background: color["400"],
191+
padding: 8,
192+
borderRadius: 8,
193+
}}
194+
>
195+
<Calendar onChange={handleChange} value={value} selectRange={true} />
196+
</div>
197+
);
198+
}
199+
186200
export default TransactionListDateRangeFilter;

src/models/transaction.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,7 @@ export type TransactionPagination = {
104104
hasNextPages: boolean;
105105
totalPages: number;
106106
};
107+
108+
export type Range<T> = [T, T];
109+
export type ValuePiece = Date | null;
110+
export type Value = ValuePiece | Range<ValuePiece>;

0 commit comments

Comments
 (0)