upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5

Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5
This commit is contained in:
Josh Hawkins 2026-03-04 19:50:52 -06:00
parent cf7535338a
commit 7626bc0344
33 changed files with 2164 additions and 2020 deletions

4035
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -63,17 +63,17 @@
"monaco-yaml": "^5.3.1", "monaco-yaml": "^5.3.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nosleep.js": "^0.12.0", "nosleep.js": "^0.12.0",
"react": "^18.3.1", "react": "^19.2.4",
"react-apexcharts": "^1.4.1", "react-apexcharts": "^1.4.1",
"react-day-picker": "^9.7.0", "react-day-picker": "^9.7.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.3.1", "react-dom": "^19.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-grid-layout": "^2.2.2", "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",
"react-konva": "^18.2.10", "react-konva": "^19.2.3",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
@ -99,9 +99,10 @@
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@testing-library/jest-dom": "^6.6.2", "@testing-library/jest-dom": "^6.6.2",
"@types/lodash": "^4.17.12", "@types/lodash": "^4.17.12",
"monaco-editor": "^0.52.0",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.2", "@types/react": "^19.2.14",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^19.2.3",
"@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",
@ -114,7 +115,7 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.2.0", "eslint-plugin-jest": "^28.2.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.8", "eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-vitest-globals": "^1.5.0", "eslint-plugin-vitest-globals": "^1.5.0",
"fake-indexeddb": "^6.0.0", "fake-indexeddb": "^6.0.0",

View File

@ -114,10 +114,17 @@ interface PropertyElement {
content: React.ReactElement; content: React.ReactElement;
} }
/** Shape of the props that RJSF injects into each property element. */
interface RjsfElementProps {
schema?: { type?: string | string[] };
uiSchema?: Record<string, unknown> & {
"ui:widget"?: string;
"ui:options"?: Record<string, unknown>;
};
}
function isObjectLikeElement(item: PropertyElement) { function isObjectLikeElement(item: PropertyElement) {
const fieldSchema = item.content.props?.schema as const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
| { type?: string | string[] }
| undefined;
return fieldSchema?.type === "object"; return fieldSchema?.type === "object";
} }
@ -163,16 +170,21 @@ function GridLayoutObjectFieldTemplate(
// Override the properties rendering with grid layout // Override the properties rendering with grid layout
const isHiddenProp = (prop: (typeof properties)[number]) => const isHiddenProp = (prop: (typeof properties)[number]) =>
prop.content.props.uiSchema?.["ui:widget"] === "hidden"; (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
"hidden";
const visibleProps = properties.filter((prop) => !isHiddenProp(prop)); const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
// Separate regular and advanced properties // Separate regular and advanced properties
const advancedProps = visibleProps.filter( const advancedProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true, (p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced === true,
); );
const regularProps = visibleProps.filter( const regularProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true, (p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced !== true,
); );
const hasModifiedAdvanced = advancedProps.some((prop) => const hasModifiedAdvanced = advancedProps.some((prop) =>
isPathModified([...fieldPath, prop.name]), isPathModified([...fieldPath, prop.name]),

View File

@ -448,6 +448,12 @@ export function FieldTemplate(props: FieldTemplateProps) {
); );
}; };
const errorsProps = errors?.props as
| { errors?: unknown[] }
| undefined;
const hasFieldErrors =
!!errors && (errorsProps?.errors?.length ?? 0) > 0;
const renderStandardLabel = () => { const renderStandardLabel = () => {
if (!shouldRenderStandardLabel) { if (!shouldRenderStandardLabel) {
return null; return null;
@ -459,7 +465,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
isModified && "text-danger", isModified && "text-danger",
errors && errors.props?.errors?.length > 0 && "text-destructive", hasFieldErrors && "text-destructive",
)} )}
> >
{finalLabel} {finalLabel}
@ -497,7 +503,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
isModified && "text-danger", isModified && "text-danger",
errors && errors.props?.errors?.length > 0 && "text-destructive", hasFieldErrors && "text-destructive",
)} )}
> >
{finalLabel} {finalLabel}

View File

@ -1,6 +1,7 @@
// Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields // Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields
// Renders simple nullable types as single inputs instead of dropdowns // Renders simple nullable types as single inputs instead of dropdowns
import type { JSX } from "react";
import { import {
MultiSchemaFieldTemplateProps, MultiSchemaFieldTemplateProps,
StrictRJSFSchema, StrictRJSFSchema,

View File

@ -25,6 +25,15 @@ import {
import get from "lodash/get"; import get from "lodash/get";
import { AddPropertyButton, AdvancedCollapsible } from "../components"; import { AddPropertyButton, AdvancedCollapsible } from "../components";
/** Shape of the props that RJSF injects into each property element. */
interface RjsfElementProps {
schema?: { type?: string | string[] };
uiSchema?: Record<string, unknown> & {
"ui:widget"?: string;
"ui:options"?: Record<string, unknown>;
};
}
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const { const {
title, title,
@ -182,16 +191,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
uiSchema?.["ui:options"]?.disableNestedCard === true; uiSchema?.["ui:options"]?.disableNestedCard === true;
const isHiddenProp = (prop: (typeof properties)[number]) => const isHiddenProp = (prop: (typeof properties)[number]) =>
prop.content.props.uiSchema?.["ui:widget"] === "hidden"; (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
"hidden";
const visibleProps = properties.filter((prop) => !isHiddenProp(prop)); const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
// Check for advanced section grouping // Check for advanced section grouping
const advancedProps = visibleProps.filter( const advancedProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true, (p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced === true,
); );
const regularProps = visibleProps.filter( const regularProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true, (p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced !== true,
); );
const hasModifiedAdvanced = advancedProps.some((prop) => const hasModifiedAdvanced = advancedProps.some((prop) =>
checkSubtreeModified([...fieldPath, prop.name]), checkSubtreeModified([...fieldPath, prop.name]),
@ -333,9 +347,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const ungrouped = items.filter((item) => !grouped.has(item.name)); const ungrouped = items.filter((item) => !grouped.has(item.name));
const isObjectLikeField = (item: (typeof properties)[number]) => { const isObjectLikeField = (item: (typeof properties)[number]) => {
const fieldSchema = item.content.props.schema as const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
| { type?: string | string[] }
| undefined;
return fieldSchema?.type === "object"; return fieldSchema?.type === "object";
}; };

View File

@ -1,4 +1,5 @@
import { t } from "i18next"; import { t } from "i18next";
import type { JSX } from "react";
import { FunctionComponent, useEffect, useMemo, useState } from "react"; import { FunctionComponent, useEffect, useMemo, useState } from "react";
interface IProp { interface IProp {

View File

@ -102,7 +102,7 @@ export function MobilePagePortal({
type MobilePageContentProps = { type MobilePageContentProps = {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
scrollerRef?: React.RefObject<HTMLDivElement>; scrollerRef?: React.RefObject<HTMLDivElement | null>;
}; };
export function MobilePageContent({ export function MobilePageContent({

View File

@ -10,7 +10,7 @@ import Konva from "konva";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
type DebugDrawingLayerProps = { type DebugDrawingLayerProps = {
containerRef: React.RefObject<HTMLDivElement>; containerRef: React.RefObject<HTMLDivElement | null>;
cameraWidth: number; cameraWidth: number;
cameraHeight: number; cameraHeight: number;
}; };

View File

@ -17,7 +17,7 @@ type ObjectPathProps = {
color?: number[]; color?: number[];
width?: number; width?: number;
pointRadius?: number; pointRadius?: number;
imgRef: React.RefObject<HTMLImageElement>; imgRef: React.RefObject<HTMLImageElement | null>;
onPointClick?: (index: number) => void; onPointClick?: (index: number) => void;
visible?: boolean; visible?: boolean;
}; };

View File

@ -22,6 +22,7 @@ import {
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import type { JSX } from "react";
import { useRef } from "react"; import { useRef } from "react";
type PlatformAwareDialogProps = { type PlatformAwareDialogProps = {

View File

@ -91,7 +91,7 @@ export default function HlsVideoPlayer({
// playback // playback
const hlsRef = useRef<Hls>(); const hlsRef = useRef<Hls>(undefined);
const [useHlsCompat, setUseHlsCompat] = useState(false); const [useHlsCompat, setUseHlsCompat] = useState(false);
const [loadedMetadata, setLoadedMetadata] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>(); const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();

View File

@ -51,10 +51,10 @@ export default function WebRtcPlayer({
// camera states // camera states
const pcRef = useRef<RTCPeerConnection | undefined>(); const pcRef = useRef<RTCPeerConnection | undefined>(undefined);
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>(); const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>(); const videoLoadTimeoutRef = useRef<NodeJS.Timeout>(undefined);
const PeerConnection = useCallback( const PeerConnection = useCallback(
async (media: string) => { async (media: string) => {

View File

@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil";
import { usePolygonStates } from "@/hooks/use-polygon-states"; import { usePolygonStates } from "@/hooks/use-polygon-states";
type PolygonCanvasProps = { type PolygonCanvasProps = {
containerRef: RefObject<HTMLDivElement>; containerRef: RefObject<HTMLDivElement | null>;
camera: string; camera: string;
width: number; width: number;
height: number; height: number;

View File

@ -18,7 +18,7 @@ import Konva from "konva";
import { Vector2d } from "konva/lib/types"; import { Vector2d } from "konva/lib/types";
type PolygonDrawerProps = { type PolygonDrawerProps = {
stageRef: RefObject<Konva.Stage>; stageRef: RefObject<Konva.Stage | null>;
points: number[][]; points: number[][];
distances: number[]; distances: number[];
isActive: boolean; isActive: boolean;

View File

@ -37,8 +37,8 @@ export type EventReviewTimelineProps = {
events: ReviewSegment[]; events: ReviewSegment[];
visibleTimestamps?: number[]; visibleTimestamps?: number[];
severityType: ReviewSeverity; severityType: ReviewSeverity;
timelineRef?: RefObject<HTMLDivElement>; timelineRef?: RefObject<HTMLDivElement | null>;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement | null>;
onHandlebarDraggingChange?: (isDragging: boolean) => void; onHandlebarDraggingChange?: (isDragging: boolean) => void;
isZooming: boolean; isZooming: boolean;
zoomDirection: TimelineZoomDirection; zoomDirection: TimelineZoomDirection;

View File

@ -28,7 +28,7 @@ type EventSegmentProps = {
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
severityType: ReviewSeverity; severityType: ReviewSeverity;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void; scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
dense: boolean; dense: boolean;

View File

@ -41,8 +41,8 @@ export type MotionReviewTimelineProps = {
events: ReviewSegment[]; events: ReviewSegment[];
motion_events: MotionData[]; motion_events: MotionData[];
noRecordingRanges?: RecordingSegment[]; noRecordingRanges?: RecordingSegment[];
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement | null>;
timelineRef?: RefObject<HTMLDivElement>; timelineRef?: RefObject<HTMLDivElement | null>;
onHandlebarDraggingChange?: (isDragging: boolean) => void; onHandlebarDraggingChange?: (isDragging: boolean) => void;
dense?: boolean; dense?: boolean;
isZooming: boolean; isZooming: boolean;

View File

@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
export type ReviewTimelineProps = { export type ReviewTimelineProps = {
timelineRef: RefObject<HTMLDivElement>; timelineRef: RefObject<HTMLDivElement | null>;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement | null>;
segmentDuration: number; segmentDuration: number;
timelineDuration: number; timelineDuration: number;
timelineStartAligned: number; timelineStartAligned: number;

View File

@ -14,7 +14,7 @@ import {
import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useTimelineUtils } from "@/hooks/use-timeline-utils";
export type SummaryTimelineProps = { export type SummaryTimelineProps = {
reviewTimelineRef: React.RefObject<HTMLDivElement>; reviewTimelineRef: React.RefObject<HTMLDivElement | null>;
timelineStart: number; timelineStart: number;
timelineEnd: number; timelineEnd: number;
segmentDuration: number; segmentDuration: number;

View File

@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { ReviewSegment, ReviewSeverity } from "@/types/review";
type VirtualizedEventSegmentsProps = { type VirtualizedEventSegmentsProps = {
timelineRef: React.RefObject<HTMLDivElement>; timelineRef: React.RefObject<HTMLDivElement | null>;
segments: number[]; segments: number[];
events: ReviewSegment[]; events: ReviewSegment[];
segmentDuration: number; segmentDuration: number;
@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = {
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
severityType: ReviewSeverity; severityType: ReviewSeverity;
contentRef: React.RefObject<HTMLDivElement>; contentRef: React.RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean; dense: boolean;
alignStartDateToTimeline: (timestamp: number) => number; alignStartDateToTimeline: (timestamp: number) => number;

View File

@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment";
import { ReviewSegment, MotionData } from "@/types/review"; import { ReviewSegment, MotionData } from "@/types/review";
type VirtualizedMotionSegmentsProps = { type VirtualizedMotionSegmentsProps = {
timelineRef: React.RefObject<HTMLDivElement>; timelineRef: React.RefObject<HTMLDivElement | null>;
segments: number[]; segments: number[];
events: ReviewSegment[]; events: ReviewSegment[];
motion_events: MotionData[]; motion_events: MotionData[];
@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = {
showMinimap: boolean; showMinimap: boolean;
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
contentRef: React.RefObject<HTMLDivElement>; contentRef: React.RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean; dense: boolean;
motionOnly: boolean; motionOnly: boolean;

View File

@ -1,3 +1,4 @@
import type { JSX } from "react";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Button } from "./button"; import { Button } from "./button";
import { Calendar } from "./calendar"; import { Calendar } from "./calendar";
@ -124,8 +125,8 @@ export function DateRangePicker({
); );
// Refs to store the values of range and rangeCompare when the date picker is opened // Refs to store the values of range and rangeCompare when the date picker is opened
const openedRangeRef = useRef<DateRange | undefined>(); const openedRangeRef = useRef<DateRange | undefined>(undefined);
const openedRangeCompareRef = useRef<DateRange | undefined>(); const openedRangeCompareRef = useRef<DateRange | undefined>(undefined);
const [selectedPreset, setSelectedPreset] = useState<string | undefined>( const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
undefined, undefined,

View File

@ -1,10 +1,10 @@
import { ForwardedRef, forwardRef } from "react"; import { ForwardedRef, forwardRef } from "react";
import { IconType } from "react-icons"; import { IconType } from "react-icons";
interface IconWrapperProps { interface IconWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
icon: IconType; icon: IconType;
className?: string; className?: string;
[key: string]: any; disabled?: boolean;
} }
const IconWrapper = forwardRef( const IconWrapper = forwardRef(

View File

@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
import useUserInteraction from "./use-user-interaction"; import useUserInteraction from "./use-user-interaction";
type DraggableElementProps = { type DraggableElementProps = {
contentRef: React.RefObject<HTMLElement>; contentRef: React.RefObject<HTMLElement | null>;
timelineRef: React.RefObject<HTMLDivElement>; timelineRef: React.RefObject<HTMLDivElement | null>;
segmentsRef: React.RefObject<HTMLDivElement>; segmentsRef: React.RefObject<HTMLDivElement | null>;
draggableElementRef: React.RefObject<HTMLDivElement>; draggableElementRef: React.RefObject<HTMLDivElement | null>;
segmentDuration: number; segmentDuration: number;
showDraggableElement: boolean; showDraggableElement: boolean;
draggableElementTime?: number; draggableElementTime?: number;

View File

@ -78,7 +78,7 @@ function removeEventListeners(
} }
export function useFullscreen<T extends HTMLElement = HTMLElement>( export function useFullscreen<T extends HTMLElement = HTMLElement>(
elementRef: RefObject<T>, elementRef: RefObject<T | null>,
) { ) {
const [fullscreen, setFullscreen] = useState<boolean>(false); const [fullscreen, setFullscreen] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
const useImageLoaded = (): [ const useImageLoaded = (): [
React.RefObject<HTMLImageElement>, React.RefObject<HTMLImageElement | null>,
boolean, boolean,
() => void, () => void,
] => { ] => {

View File

@ -3,7 +3,7 @@ import { useCallback } from "react";
export type TimelineUtilsProps = { export type TimelineUtilsProps = {
segmentDuration: number; segmentDuration: number;
timelineDuration?: number; timelineDuration?: number;
timelineRef?: React.RefObject<HTMLElement>; timelineRef?: React.RefObject<HTMLElement | null>;
}; };
export function useTimelineUtils({ export function useTimelineUtils({

View File

@ -11,7 +11,7 @@ type UseTimelineZoomProps = {
zoomLevels: ZoomSettings[]; zoomLevels: ZoomSettings[];
onZoomChange: (newZoomLevel: number) => void; onZoomChange: (newZoomLevel: number) => void;
pinchThresholdPercent?: number; pinchThresholdPercent?: number;
timelineRef: React.RefObject<HTMLDivElement>; timelineRef: React.RefObject<HTMLDivElement | null>;
timelineDuration: number; timelineDuration: number;
}; };

View File

@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
type UseUserInteractionProps = { type UseUserInteractionProps = {
elementRef: React.RefObject<HTMLElement>; elementRef: React.RefObject<HTMLElement | null>;
}; };
function useUserInteraction({ elementRef }: UseUserInteractionProps) { function useUserInteraction({ elementRef }: UseUserInteractionProps) {
const [userInteracting, setUserInteracting] = useState(false); const [userInteracting, setUserInteracting] = useState(false);
const interactionTimeout = useRef<NodeJS.Timeout>(); const interactionTimeout = useRef<NodeJS.Timeout>(undefined);
const isProgrammaticScroll = useRef(false); const isProgrammaticScroll = useRef(false);
const setProgrammaticScroll = useCallback(() => { const setProgrammaticScroll = useCallback(() => {

View File

@ -7,7 +7,7 @@ export type VideoResolutionType = {
}; };
export function useVideoDimensions( export function useVideoDimensions(
containerRef: React.RefObject<HTMLDivElement>, containerRef: React.RefObject<HTMLDivElement | null>,
) { ) {
const [{ width: containerWidth, height: containerHeight }] = const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef); useResizeObserver(containerRef);

View File

@ -56,7 +56,7 @@ type DraggableGridLayoutProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
cameraGroup: string; cameraGroup: string;
cameraRef: (node: HTMLElement | null) => void; cameraRef: (node: HTMLElement | null) => void;
containerRef: React.RefObject<HTMLDivElement>; containerRef: React.RefObject<HTMLDivElement | null>;
includeBirdseye: boolean; includeBirdseye: boolean;
onSelectCamera: (camera: string) => void; onSelectCamera: (camera: string) => void;
windowVisible: boolean; windowVisible: boolean;

View File

@ -419,7 +419,7 @@ export default function SearchView({
>(); >();
// keep track of previous ref to outline thumbnail when dialog closes // keep track of previous ref to outline thumbnail when dialog closes
const prevSearchDetailRef = useRef<SearchResult | undefined>(); const prevSearchDetailRef = useRef<SearchResult | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (searchDetail === undefined && prevSearchDetailRef.current) { if (searchDetail === undefined && prevSearchDetailRef.current) {