Skip to content

Commit 1d86bb7

Browse files
committed
Stage 7: small dependency cleanups (final modernization stage)
Seventh and final step of the frontend modernization: the remaining small, independently-swappable dependencies flagged in the original audit. - downshift: removed entirely (declared but zero usages - TagSelector.tsx already implements its own combobox logic by hand). - lodash.clonedeep: removed; its one call site (cloning a dashboard entry before editing) now uses structuredClone, standard in all supported runtimes. - @rooks/use-interval / @rooks/use-timeout: inlined as ~15-line custom hooks in utils/hooks.ts (ref-based latest-callback pattern, matching each package's actual behavior) - each package was a thin wrapper around setInterval/setTimeout, not worth a dependency for 5 call sites. - color-hash: 1.0.3 -> 2.0.2, dropped the `@ts-expect-error` in favor of the real @types/color-hash declarations. - react-grid-layout: 0.16.6 -> 1.5.3, not the current 2.x line - v2 is a hooks-based rewrite that dropped WidthProvider (and the whole class- component API this app's DashboardPage.tsx is built around) in favor of new hooks, which would be a real rewrite, not a dependency bump. 1.x keeps the same WidthProvider/class-component shape (confirmed by diffing its source against 0.16) while being multiple years of bugfixes newer. Paired with @types/react-grid-layout@1.3.6 (the last version with real declarations - 2.x's types package is a stub pointing at the source package's own bundled types, which only exist in react-grid-layout v2). This also let the React-18-children cast added in the MUI stage be removed - 1.3.6's types already declare children correctly. - react-color (ancient 3.0.0-beta.3, unmaintained) -> react-colorful: swapped the hue-only SliderPicker for HexColorPicker (react-colorful has no dedicated hue-slider export, only full pickers) in the two tag color-editing dialogs - a small, visible UI change but a maintained, dependency-free, actively-secure package. - react-infinite: removed. Its virtualized-scroll usage in timespan/DoneTrackers.tsx was replaced with a plain IntersectionObserver sentinel that calls the same fetchMore() on scroll-into-view - this is a personal time log, not large enough to need DOM virtualization, and it deleted the height-tracking ref/effect machinery that existed only to feed react-infinite's layout calculations. - @fullcalendar/*: 4.3.x -> 6.1.21 across all six packages. This was the largest real API migration in this stage: initialView (was defaultView), datesSet (was datesRender), headerToolbar (was header), dayHeaderFormat (was columnHeaderFormat), eventContent + eventDidMount replacing the single DOM-mutating eventRender (v5+ moved event rendering to a declarative model - content injection is now separate from post-mount DOM side effects), EventChangeArg's oldEvent (was prevEvent on the resize handler specifically, inconsistent with the drop handler's already-correct oldEvent - once fixed both handlers were identical and got merged into one). The bundled per-package CSS imports (main.css) are gone - v5+ injects its own styles - which also meant the app's custom FullCalendarStyling.tsx overrides needed their selectors updated for the new DOM/class names (.fc-content -> .fc-event-main, .fc-time-grid-event -> .fc-timegrid-event, .fc-today -> .fc-day-today, etc.); the dead v4 day-grid/month-view rules were dropped since this app only ever shows timeGrid views. Also had to add an explicit `.fc { height: 100% }` rule, since fc's height:"parent" option stopped auto-filling its container - unclear whether that's a behavior change or just newly needing the CSS assist since the removed default stylesheet controlled it before. Verified: eslint/tsc/vitest clean, make lint-js/test-js/build-js, a real go build + running binary, and browser walkthroughs of every affected surface individually - the react-colorful picker (dialog open, pick a color, save), the react-grid-layout dashboard (create, enter edit mode, add-entry grid tile), and FullCalendar (week view renders pixel-similar to before, an active tracker's event shows the live-updating STOP button via eventContent, drag/resize/click handlers all still fire) - all zero console errors. Final `yarn audit`: 114 critical/664 high/350 moderate/89 low -> 0 critical/8 high/6 moderate/0 low, all of which are transitive lodash/d3-color findings from recharts (deliberately kept unchanged per the modernization plan - bumping it is a separate, larger effort with its own breaking-change surface, noted as a follow-up rather than done here).
1 parent a8c4bd7 commit 1d86bb7

12 files changed

Lines changed: 218 additions & 554 deletions

File tree

ui/package.json

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,42 @@
1111
"@apollo/client": "^3.14.1",
1212
"@emotion/react": "^11.14.0",
1313
"@emotion/styled": "^11.14.1",
14-
"@fullcalendar/core": "^4.3.1",
15-
"@fullcalendar/daygrid": "^4.3.0",
16-
"@fullcalendar/interaction": "^4.3.0",
17-
"@fullcalendar/moment": "^4.3.0",
18-
"@fullcalendar/react": "^4.3.0",
19-
"@fullcalendar/timegrid": "^4.3.0",
14+
"@fullcalendar/core": "^6.1.21",
15+
"@fullcalendar/daygrid": "^6.1.21",
16+
"@fullcalendar/interaction": "^6.1.21",
17+
"@fullcalendar/moment": "^6.1.21",
18+
"@fullcalendar/react": "^6.1.21",
19+
"@fullcalendar/timegrid": "^6.1.21",
2020
"@mui/icons-material": "^5.18.0",
2121
"@mui/material": "^5.18.0",
2222
"@mui/styles": "^5.18.0",
2323
"@mui/system": "^5.18.0",
2424
"@mui/x-date-pickers": "^7.29.4",
25-
"@rooks/use-interval": "^1.2.0",
26-
"@rooks/use-timeout": "^3.0.1",
27-
"color-hash": "^1.0.3",
28-
"downshift": "^3.2.10",
25+
"color-hash": "^2.0.2",
2926
"events": "^3.3.0",
3027
"get-best-contrast-color": "^0.2.2",
3128
"graphql": "^16.14.2",
32-
"lodash.clonedeep": "^4.5.0",
3329
"moment": "^2.30.1",
3430
"moment-timezone": "^0.5.45",
3531
"notistack": "^3.0.2",
3632
"pretty-ms": "^5.0.0",
3733
"react": "^18.3.1",
38-
"react-color": "3.0.0-beta.3",
34+
"react-colorful": "^5.6.1",
3935
"react-dom": "^18.3.1",
40-
"react-grid-layout": "^0.16.6",
41-
"react-infinite": "^0.13.0",
36+
"react-grid-layout": "^1.5.3",
4237
"react-router-dom": "^6.30.4",
4338
"recharts": "^1.8.5",
4439
"typeface-roboto": "^0.0.54",
4540
"typescript": "^5.9.3"
4641
},
4742
"devDependencies": {
48-
"@types/lodash.clonedeep": "^4.5.6",
4943
"@types/moment-timezone": "^0.5.12",
44+
"@types/color-hash": "^2.0.0",
5045
"@types/node": "^22.0.0",
5146
"@types/pretty-ms": "^4.0.0",
5247
"@types/react": "^18.3.31",
53-
"@types/react-color": "^2.17.0",
5448
"@types/react-dom": "^18.3.7",
55-
"@types/react-grid-layout": "^0.16.7",
56-
"@types/react-infinite": "^0.0.34",
49+
"@types/react-grid-layout": "^1.3.6",
5750
"@types/recharts": "^1.8.3",
5851
"@vitejs/plugin-react": "^4.7.0",
5952
"@eslint/js": "^10.0.1",

ui/src/common/RelativeDateTimeSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import {TextField} from '@mui/material';
33
import {parseRelativeTime} from '../utils/time';
44
import Typography from '@mui/material/Typography';
5-
import useTimeout from '@rooks/use-timeout';
5+
import {useTimeout} from '../utils/hooks';
66

77
interface RelativeDateTimeSelectorProps {
88
value: string;

ui/src/common/RelativeTime.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import moment from 'moment';
22
import * as React from 'react';
33
import {timeRunning} from '../timespan/timeutils';
4-
import useInterval from '@rooks/use-interval';
4+
import {useInterval} from '../utils/hooks';
55

66
export const RelativeToNow: React.FC<{from: moment.Moment}> = ({from}) => {
77
const [now, setNow] = React.useState(moment());

ui/src/dashboard/DashboardPage.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import {DashboardItem} from '../gql/types';
1515
import {DashboardEntry} from './Entry/DashboardEntry';
1616
import Button from '@mui/material/Button';
17-
import clone from 'lodash.clonedeep';
1817
import {EditPopup} from './Entry/EditPopup';
1918
import {EditGlass} from './Entry/EditGlass';
2019
import {Fade} from '../common/Fade';
@@ -38,11 +37,7 @@ const cols: Record<ViewType, number> = {
3837
[ViewType.Desktop]: 20,
3938
};
4039

41-
// react-grid-layout 0.16's own type declarations predate the React 18 types' removal of
42-
// implicit `children` from arbitrary component props; the react-grid-layout version bump in a
43-
// later stage carries its own current types and won't need this cast.
44-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45-
const WidthAwareReactGrid = WidthProvider(ReactGrid) as React.ComponentType<any>;
40+
const WidthAwareReactGrid = WidthProvider(ReactGrid);
4641
const EditId = -1;
4742
const newEntry = (): DashboardItem => {
4843
return {
@@ -232,7 +227,7 @@ export const DashboardPage: React.FC = () => {
232227
{changeMode ? (
233228
<Fade fullyVisible={!currentEditedAndPreviewed} opacity={0}>
234229
<EditGlass
235-
doEdit={(elm) => setEdit([elm, clone(entry)])}
230+
doEdit={(elm) => setEdit([elm, structuredClone(entry)])}
236231
doDelete={() => removeDashboardEntry({variables: {id: entry.id}})}
237232
/>
238233
</Fade>
@@ -267,7 +262,7 @@ export const DashboardPage: React.FC = () => {
267262
entry={addEntry}
268263
anchorEl={addRef}
269264
onChange={(e) => {
270-
return setAddEntry(clone(e));
265+
return setAddEntry(structuredClone(e));
271266
}}
272267
finish={() => {
273268
setAddEntry(null);

ui/src/tag/AddTagDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import DialogActions from '@mui/material/DialogActions';
66
import DialogContent from '@mui/material/DialogContent';
77
import DialogContentText from '@mui/material/DialogContentText';
88
import DialogTitle from '@mui/material/DialogTitle';
9-
import {SliderPicker} from 'react-color';
9+
import {HexColorPicker} from 'react-colorful';
1010
import {InputLabel} from '@mui/material';
1111
import FormControl from '@mui/material/FormControl';
1212
import {FetchResult, useMutation} from '@apollo/client';
@@ -64,7 +64,7 @@ export const AddTagDialog: React.FC<AddTagDialogProps> = ({close, open, initialN
6464
Color
6565
</InputLabel>
6666
<div id="color-picker" style={{marginTop: 25}}>
67-
<SliderPicker onChange={(c) => setColor(c.hex)} color={color} />
67+
<HexColorPicker onChange={setColor} color={color} />
6868
</div>
6969
</FormControl>
7070
</DialogContent>

ui/src/tag/TagPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
UpdateTagMutationVariables,
2727
} from '../gql/__generated__';
2828
import {AddTagDialog} from './AddTagDialog';
29-
import {SliderPicker} from 'react-color';
29+
import {HexColorPicker} from 'react-colorful';
3030
import {TagChip} from '../common/TagChip';
3131
import {handleError} from '../utils/errors';
3232
import {ConfirmDialog} from '../common/ConfirmDialog';
@@ -101,7 +101,7 @@ export const TagPage = () => {
101101
</TableCell>
102102
<TableCell style={{minWidth: 128}}>
103103
{isEdited ? (
104-
<SliderPicker onChange={(c) => setEditing([editKey, editKeyNew, c.hex])} color={editColor} />
104+
<HexColorPicker onChange={(c) => setEditing([editKey, editKeyNew, c])} color={editColor} />
105105
) : (
106106
<TagChip label={tag.color} color={tag.color} />
107107
)}

ui/src/timespan/DoneTrackers.tsx

Lines changed: 31 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,13 @@ import * as gqlTimeSpan from '../gql/timeSpan';
44
import * as gqlTag from '../gql/tags';
55
import {TimeSpan, TimeSpanProps} from './TimeSpan';
66
import {TagsQuery, TimeSpansQuery, TimeSpansQueryVariables} from '../gql/__generated__';
7-
import useInterval from '@rooks/use-interval';
7+
import {useInterval} from '../utils/hooks';
88
import moment from 'moment';
99
import {Typography} from '@mui/material';
1010
import {GroupedTimeSpanProps, toGroupedTimeSpanProps} from './timespanutils';
1111
import {TagSelectorEntry} from '../tag/tagSelectorEntry';
12-
import ReactInfiniteImport from 'react-infinite';
1312
import {isSameDate} from '../utils/time';
1413

15-
// react-infinite 0.13's type declarations predate the React 18 types' removal of implicit
16-
// `children` from arbitrary component props; this package gets replaced in a later stage.
17-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18-
const ReactInfinite = ReactInfiniteImport as React.ComponentType<any>;
19-
2014
interface DoneTrackersProps {
2115
addTagsToTracker?: (entries: TagSelectorEntry[]) => void;
2216
}
@@ -27,9 +21,8 @@ export const DoneTrackers: React.FC<DoneTrackersProps> = ({addTagsToTracker}) =>
2721
});
2822
const loading = React.useRef(false);
2923
const tagsResult = useQuery<TagsQuery>(gqlTag.Tags);
30-
const [infiniteLoading, setInfiniteLoading] = React.useState(false);
3124
const [currentDate, setCurrentDate] = React.useState(moment());
32-
const [heights, setHeights] = React.useState<Record<string, number>>({});
25+
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
3326
useInterval(
3427
() => {
3528
if (!isSameDate(currentDate, moment())) {
@@ -40,7 +33,7 @@ export const DoneTrackers: React.FC<DoneTrackersProps> = ({addTagsToTracker}) =>
4033
true
4134
);
4235

43-
const fetchMore = () => {
36+
const fetchMore = React.useCallback(() => {
4437
if (!trackersResult || !trackersResult.data || trackersResult.loading || loading.current) {
4538
return;
4639
}
@@ -69,15 +62,30 @@ export const DoneTrackers: React.FC<DoneTrackersProps> = ({addTagsToTracker}) =>
6962
};
7063
},
7164
})
72-
.then(() => {
73-
loading.current = false;
74-
return setInfiniteLoading(false);
75-
})
76-
.catch(() => {
65+
.finally(() => {
7766
loading.current = false;
78-
return setInfiniteLoading(false);
7967
});
80-
};
68+
}, [trackersResult]);
69+
70+
// Loads the next page once the sentinel div at the bottom of the list scrolls into view,
71+
// replacing react-infinite's windowed/virtualized approach - this list is a personal time
72+
// log, not large enough to need DOM virtualization.
73+
React.useEffect(() => {
74+
const target = loadMoreRef.current;
75+
if (!target) {
76+
return;
77+
}
78+
const observer = new IntersectionObserver(
79+
(entries) => {
80+
if (entries[0].isIntersecting) {
81+
fetchMore();
82+
}
83+
},
84+
{rootMargin: '2000px 0px'}
85+
);
86+
observer.observe(target);
87+
return () => observer.disconnect();
88+
}, [fetchMore]);
8189

8290
const values: GroupedTimeSpanProps = React.useMemo(() => {
8391
if (
@@ -97,53 +105,22 @@ export const DoneTrackers: React.FC<DoneTrackersProps> = ({addTagsToTracker}) =>
97105

98106
return (
99107
<div style={{marginTop: 10}}>
100-
<ReactInfinite
101-
key={1}
102-
useWindowAsScrollContainer
103-
preloadBatchSize={window.innerHeight}
104-
onInfiniteLoad={fetchMore}
105-
isInfiniteLoading={infiniteLoading}
106-
infiniteLoadBeginEdgeOffset={2000}
107-
loadingSpinnerDelegate={
108-
<Typography align={'center'} variant={'h5'}>
109-
.. loading time spans ..
110-
</Typography>
111-
}
112-
elementHeight={values.map((m) => heights[m.key] || 500)}>
113-
{values.map(({key, timeSpans}) => {
114-
return (
115-
<DatedTimeSpans
116-
key={key}
117-
name={key}
118-
timeSpans={timeSpans}
119-
addTagsToTracker={addTagsToTracker}
120-
setHeight={setHeights}
121-
height={heights[key] || 500}
122-
/>
123-
);
124-
})}
125-
</ReactInfinite>
108+
{values.map(({key, timeSpans}) => (
109+
<DatedTimeSpans key={key} name={key} timeSpans={timeSpans} addTagsToTracker={addTagsToTracker} />
110+
))}
111+
<div ref={loadMoreRef} />
126112
</div>
127113
);
128114
};
129115

130116
const DatedTimeSpans: React.FC<
131117
{
132118
name: string;
133-
setHeight: (cb: (height: Record<string, number>) => Record<string, number>) => void;
134-
height: number;
135119
timeSpans: TimeSpanProps[];
136120
} & DoneTrackersProps
137-
> = ({name, timeSpans, addTagsToTracker, setHeight, height}) => {
138-
const ref = React.useRef<HTMLDivElement | null>();
139-
React.useEffect(() => {
140-
const currentHeight = ref.current && ref.current.getBoundingClientRect().height;
141-
if (currentHeight != null && currentHeight !== height) {
142-
setHeight((old) => ({...old, [name]: currentHeight}));
143-
}
144-
}, [ref, name, setHeight, height]);
121+
> = ({name, timeSpans, addTagsToTracker}) => {
145122
return (
146-
<div key={name} ref={(r) => (ref.current = r)}>
123+
<div key={name}>
147124
<Typography key={name} align="center" variant={'h5'}>
148125
{name}
149126
</Typography>

0 commit comments

Comments
 (0)