migrate react-grid-layout v1 to v2

- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)
This commit is contained in:
Josh Hawkins 2026-03-04 19:36:59 -06:00
parent 638ee3bd0a
commit 2e66902fe4
3 changed files with 215 additions and 199 deletions

51
web/package-lock.json generated
View File

@ -63,7 +63,7 @@
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-grid-layout": "^1.5.0", "react-grid-layout": "^2.2.2",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.52.1",
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -96,7 +96,6 @@
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.2", "@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0", "@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@types/strftime": "^0.9.8", "@types/strftime": "^0.9.8",
@ -5223,15 +5222,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-grid-layout": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz",
"integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-icons": { "node_modules/@types/react-icons": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz",
@ -10959,11 +10949,12 @@
} }
}, },
"node_modules/react-draggable": { "node_modules/react-draggable": {
"version": "4.4.6", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
"license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^1.1.1", "clsx": "^2.1.1",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
}, },
"peerDependencies": { "peerDependencies": {
@ -10971,14 +10962,6 @@
"react-dom": ">= 16.3.0" "react-dom": ">= 16.3.0"
} }
}, },
"node_modules/react-draggable/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/react-dropzone": { "node_modules/react-dropzone": {
"version": "14.3.8", "version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
@ -10997,15 +10980,15 @@
} }
}, },
"node_modules/react-grid-layout": { "node_modules/react-grid-layout": {
"version": "1.5.0", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz",
"integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^2.0.0", "clsx": "^2.1.1",
"fast-equals": "^4.0.3", "fast-equals": "^4.0.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-draggable": "^4.4.5", "react-draggable": "^4.4.6",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"resize-observer-polyfill": "^1.5.1" "resize-observer-polyfill": "^1.5.1"
}, },
@ -11188,15 +11171,17 @@
} }
}, },
"node_modules/react-resizable": { "node_modules/react-resizable": {
"version": "3.0.5", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz",
"integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==",
"license": "MIT",
"dependencies": { "dependencies": {
"prop-types": "15.x", "prop-types": "15.x",
"react-draggable": "^4.0.3" "react-draggable": "^4.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">= 16.3" "react": ">= 16.3",
"react-dom": ">= 16.3"
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {

View File

@ -69,7 +69,7 @@
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-grid-layout": "^1.5.0", "react-grid-layout": "^2.2.2",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.52.1",
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -102,7 +102,6 @@
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.2", "@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0", "@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@types/strftime": "^0.9.8", "@types/strftime": "^0.9.8",

View File

@ -14,10 +14,10 @@ import React, {
useState, useState,
} from "react"; } from "react";
import { import {
ItemCallback,
Layout, Layout,
LayoutItem,
Responsive, Responsive,
WidthProvider, useContainerWidth,
} from "react-grid-layout"; } from "react-grid-layout";
import "react-grid-layout/css/styles.css"; import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css"; import "react-resizable/css/styles.css";
@ -116,11 +116,8 @@ export default function DraggableGridLayout({
// grid layout // grid layout
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []); const [gridLayout, setGridLayout, isGridLayoutLoaded] =
useUserPersistence<Layout>(`${cameraGroup}-draggable-layout`);
const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence<
Layout[]
>(`${cameraGroup}-draggable-layout`);
const [group] = useUserPersistedOverlayState( const [group] = useUserPersistedOverlayState(
"cameraGroup", "cameraGroup",
@ -158,11 +155,11 @@ export default function DraggableGridLayout({
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
useState<boolean>(); useState<boolean>();
const [currentGridLayout, setCurrentGridLayout] = useState< const [currentGridLayout, setCurrentGridLayout] = useState<
Layout[] | undefined Layout | undefined
>(); >();
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(currentLayout: Layout[]) => { (currentLayout: Layout) => {
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) { if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
return; return;
} }
@ -174,7 +171,7 @@ export default function DraggableGridLayout({
); );
const generateLayout = useCallback( const generateLayout = useCallback(
(baseLayout: Layout[] | undefined) => { (baseLayout: Layout | undefined) => {
if (!isGridLayoutLoaded) { if (!isGridLayoutLoaded) {
return; return;
} }
@ -184,7 +181,7 @@ export default function DraggableGridLayout({
? ["birdseye", ...cameras.map((camera) => camera?.name || "")] ? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
: cameras.map((camera) => camera?.name || ""); : cameras.map((camera) => camera?.name || "");
const optionsMap: Layout[] = baseLayout const optionsMap: LayoutItem[] = baseLayout
? baseLayout.filter((layout) => cameraNames?.includes(layout.i)) ? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
: []; : [];
@ -325,6 +322,24 @@ export default function DraggableGridLayout({
const gridContainerRef = useRef<HTMLDivElement>(null); const gridContainerRef = useRef<HTMLDivElement>(null);
const {
width: gridWidth,
containerRef: gridWidthRef,
mounted: gridMounted,
} = useContainerWidth();
// Combine gridContainerRef and gridWidthRef into a single callback ref
const combinedGridRef = useCallback(
(node: HTMLDivElement | null) => {
(
gridContainerRef as React.MutableRefObject<HTMLDivElement | null>
).current = node;
(gridWidthRef as React.MutableRefObject<HTMLDivElement | null>).current =
node;
},
[gridWidthRef],
);
const [{ width: containerWidth, height: containerHeight }] = const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(gridContainerRef); useResizeObserver(gridContainerRef);
@ -363,12 +378,14 @@ export default function DraggableGridLayout({
); );
}, [availableWidth, marginValue]); }, [availableWidth, marginValue]);
const handleResize: ItemCallback = ( const handleResize = (
_: Layout[], _layout: Layout,
oldLayoutItem: Layout, oldLayoutItem: LayoutItem | null,
layoutItem: Layout, layoutItem: LayoutItem | null,
placeholder: Layout, placeholder: LayoutItem | null,
) => { ) => {
if (!oldLayoutItem || !layoutItem || !placeholder) return;
const heightDiff = layoutItem.h - oldLayoutItem.h; const heightDiff = layoutItem.h - oldLayoutItem.h;
const widthDiff = layoutItem.w - oldLayoutItem.w; const widthDiff = layoutItem.w - oldLayoutItem.w;
const changeCoef = oldLayoutItem.w / oldLayoutItem.h; const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
@ -529,7 +546,7 @@ export default function DraggableGridLayout({
) : ( ) : (
<div <div
className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8" className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
ref={gridContainerRef} ref={combinedGridRef}
> >
<EditGroupDialog <EditGroupDialog
open={editGroup} open={editGroup}
@ -537,8 +554,10 @@ export default function DraggableGridLayout({
currentGroups={groups} currentGroups={groups}
activeGroup={group} activeGroup={group}
/> />
<ResponsiveGridLayout {gridMounted && (
<Responsive
className="grid-layout" className="grid-layout"
width={gridWidth}
layouts={{ layouts={{
lg: currentGridLayout, lg: currentGridLayout,
md: currentGridLayout, md: currentGridLayout,
@ -551,13 +570,17 @@ export default function DraggableGridLayout({
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }} cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
margin={[marginValue, marginValue]} margin={[marginValue, marginValue]}
containerPadding={[0, isEditMode ? 6 : 3]} containerPadding={[0, isEditMode ? 6 : 3]}
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []} resizeConfig={{
enabled: isEditMode,
handles: isEditMode ? ["sw", "nw", "se", "ne"] : [],
}}
dragConfig={{
enabled: isEditMode,
}}
onDragStop={handleLayoutChange} onDragStop={handleLayoutChange}
onResize={handleResize} onResize={handleResize}
onResizeStart={() => setShowCircles(false)} onResizeStart={() => setShowCircles(false)}
onResizeStop={handleLayoutChange} onResizeStop={handleLayoutChange}
isDraggable={isEditMode}
isResizable={isEditMode}
> >
{includeBirdseye && birdseyeConfig?.enabled && ( {includeBirdseye && birdseyeConfig?.enabled && (
<BirdseyeLivePlayerGridItem <BirdseyeLivePlayerGridItem
@ -585,10 +608,12 @@ export default function DraggableGridLayout({
grow = "aspect-video"; grow = "aspect-video";
} }
const availableStreams = camera.live.streams || {}; const availableStreams = camera.live.streams || {};
const firstStreamEntry = Object.values(availableStreams)[0] || ""; const firstStreamEntry =
Object.values(availableStreams)[0] || "";
const streamNameFromSettings = const streamNameFromSettings =
currentGroupStreamingSettings?.[camera.name]?.streamName || ""; currentGroupStreamingSettings?.[camera.name]?.streamName ||
"";
const streamExists = const streamExists =
streamNameFromSettings && streamNameFromSettings &&
Object.values(availableStreams).includes( Object.values(availableStreams).includes(
@ -645,7 +670,9 @@ export default function DraggableGridLayout({
key={camera.name} key={camera.name}
streamName={streamName} streamName={streamName}
autoLive={autoLive ?? globalAutoLive} autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={showStillWithoutActivity ?? true} showStillWithoutActivity={
showStillWithoutActivity ?? true
}
alwaysShowCameraName={displayCameraNames} alwaysShowCameraName={displayCameraNames}
useWebGL={useWebGL} useWebGL={useWebGL}
cameraRef={cameraRef} cameraRef={cameraRef}
@ -660,7 +687,9 @@ export default function DraggableGridLayout({
windowVisible && visibleCameras.includes(camera.name) windowVisible && visibleCameras.includes(camera.name)
} }
cameraConfig={camera} cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"} preferredLiveMode={
preferredLiveModes[camera.name] ?? "mse"
}
playInBackground={false} playInBackground={false}
showStats={statsStates[camera.name]} showStats={statsStates[camera.name]}
onClick={() => { onClick={() => {
@ -677,7 +706,9 @@ export default function DraggableGridLayout({
return newModes; return newModes;
}); });
}} }}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)} onResetLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
playAudio={audioStates[camera.name]} playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name]} volume={volumeStates[camera.name]}
/> />
@ -685,7 +716,8 @@ export default function DraggableGridLayout({
</GridLiveContextMenu> </GridLiveContextMenu>
); );
})} })}
</ResponsiveGridLayout> </Responsive>
)}
{isDesktop && ( {isDesktop && (
<div <div
className={cn( className={cn(