Fix linter errors#20
Conversation
…nd optimize state management for drag and wall loading logic
…agement and improve performance with useMemo and useCallback
…tore.ts. replacing any across all files
…ove manufacturer handling
There was a problem hiding this comment.
Code Review
This pull request focuses on improving type safety and state management across the editor feature by replacing 'any' types with explicit interfaces and refactoring several components to use React hooks like useMemo and useCallback. Key changes include the introduction of centralized interfaces for session and hold data in the store, as well as more robust loading logic for wall models in the MainCanvas. Feedback is provided regarding a direct object mutation within a useMemo hook in EditorApp.tsx, which should be replaced with an immutable update to avoid side effects on cached data. Additionally, an unnecessary type cast in AddHoldModal.tsx was identified that could lead to bugs if IDs are strings.
| session_data.holds_collection_instances?.forEach((hold: SessionHoldInstance) => { | ||
| hold.hold_instance_id = hold.id; | ||
| holdModels.push(hold); | ||
| if (hold.hold_type?.glb_url) { | ||
| holdModelsGLBURL.push(hold.hold_type.glb_url); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Mutating the hold object directly inside useMemo is a side effect that should be avoided. Since session_data likely comes from a cache (e.g., TanStack Query), mutating its contents can lead to unexpected behavior in other parts of the application or when the data is re-read from the cache. It is better to create a new object with the added property to maintain immutability.
| session_data.holds_collection_instances?.forEach((hold: SessionHoldInstance) => { | |
| hold.hold_instance_id = hold.id; | |
| holdModels.push(hold); | |
| if (hold.hold_type?.glb_url) { | |
| holdModelsGLBURL.push(hold.hold_type.glb_url); | |
| } | |
| }); | |
| session_data.holds_collection_instances?.forEach((hold: SessionHoldInstance) => { | |
| const holdWithId = { ...hold, hold_instance_id: hold.id }; | |
| holdModels.push(holdWithId); | |
| if (holdWithId.hold_type?.glb_url) { | |
| holdModelsGLBURL.push(holdWithId.hold_type.glb_url); | |
| } | |
| }); |
| (hold: StockItem) => | ||
| !currentHoldTypeIds.has(hold.hold_type.id) && | ||
| !addedHoldIds.has(hold.hold_type.id) | ||
| !addedHoldIds.has(hold.hold_type.id as number) |
There was a problem hiding this comment.
The type cast as number is unnecessary and potentially incorrect. hold.hold_type.id is defined as string | number, and addedHoldIds is a Set<string | number>. Casting to number will cause issues if the ID is actually a string.
| !addedHoldIds.has(hold.hold_type.id as number) | |
| !addedHoldIds.has(hold.hold_type.id) |
There was a problem hiding this comment.
Pull request overview
This PR addresses frontend lint/TypeScript issues in the editor feature by tightening types, removing any usage, and adjusting a few React/Three.js patterns to satisfy lint rules.
Changes:
- Centralizes and reuses editor model types (
HoldType,HoldModel,SessionData, etc.) fromfrontend/src/features/editor/store.ts. - Replaces multiple
anyusages with explicit types and safer property access across editor utils/components. - Makes targeted lint-driven refactors (e.g.,
catchwithout unused vars, const correctness, react-refresh lint suppression).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/shared/auth/AuthProvider.tsx | Adds react-refresh lint suppression for exported hook in same module. |
| frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx | Adds local types, removes any, and improves manufacturer handling. |
| frontend/src/features/editor/utils/HandleLoadSession.tsx | Introduces SessionData typing and typed layout objects. |
| frontend/src/features/editor/utils/HandleAddHold.tsx | Switches to shared store types and returns a typed HoldModel. |
| frontend/src/features/editor/stubs/Hold360.tsx | Adds react-refresh lint suppression and types hold access. |
| frontend/src/features/editor/store.ts | Introduces shared editor interfaces/types for holds and sessions. |
| frontend/src/features/editor/components/useDragPreview.ts | Cleans up const usage, removes unused catch var, improves typing. |
| frontend/src/features/editor/components/SidebarHoldsSection.tsx | Refactors local state handling (add/delete), improves typing, memoization/callbacks. |
| frontend/src/features/editor/components/Sidebar.tsx | Uses shared types and changes how currentHolds is derived for the modal. |
| frontend/src/features/editor/components/ModelViewer.tsx | Replaces any material access with MeshStandardMaterial-typed access. |
| frontend/src/features/editor/components/MainCanvas.tsx | Refactors wall loading state logic and removes some derived state effects. |
| frontend/src/features/editor/components/HoldInspector.tsx | Replaces as any listener casts with typed EventListener assertions. |
| frontend/src/features/editor/components/FileManager.tsx | Types session_data and removes any from object cleaning logic. |
| frontend/src/features/editor/components/AddHoldModal.tsx | Adds SessionData typing + StockItem type and reduces any usage. |
| frontend/src/features/editor/EditorApp.tsx | Replaces mutable in-render arrays with a useMemo-derived model list. |
Comments suppressed due to low confidence (1)
frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx:92
current_locationuses`Wall ${stockHold.last_used_wall?.wall_id}`as a fallback value, but that template string is always truthy (even whenwall_idisundefined), which prevents the final "Unknown location" fallback from ever being used and can yield "Wall undefined". Only use the wall-id string whenwall_idis present; otherwise fall through to the final fallback.
})(),
required_count: requiredCount,
available_count: availableCount,
current_location:
stockHold.last_used_wall?.wall_name ||
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| onClose={() => setAddHoldModalOpen(false)} | ||
| session_data={session_data} | ||
| onHoldAdded={handleHoldAddedFromModal} | ||
| currentHolds={ | ||
| holdsSectionRef.current?.getCurrentHolds() || processedHoldModels | ||
| } | ||
| currentHolds={[...processedHoldModels, ...locallyAddedHolds]} | ||
| /> |
There was a problem hiding this comment.
currentHolds is derived from processedHoldModels plus locallyAddedHolds, but it doesn't reflect deletions performed inside SidebarHoldsSection (which tracks deletions locally). That can cause AddHoldModal to incorrectly treat deleted hold types as still present and hide them from the stock list. Consider lifting the single source of truth for current holds to the parent (e.g., have SidebarHoldsSection notify on delete/add), or revert to using holdsSectionRef.current?.getCurrentHolds() when opening/feeding the modal so it reflects the actual sidebar state.
|
|
||
| const currentHoldData = stockData?.holds?.find( | ||
| (stock: any) => stock.id === hold.id | ||
| (stock) => stock.id === hold.id |
There was a problem hiding this comment.
The stock lookup compares stock.id === hold.id using strict equality, but stock.id is typed as string | number while hold.id is a string in this flow. If the API returns numeric IDs, this will fail and show incorrect availability/usage counts. Normalize both sides (e.g., compare stringified IDs) before comparing.
| (stock) => stock.id === hold.id | |
| (stock) => String(stock.id) === String(hold.id) |
| hold_instance_id: String(selectedHold.id), | ||
| id: String(selectedHold.id), | ||
| name: (selectedHold.hold_type.manufacturer_ref as string | undefined) | ||
| ?? `${(selectedHold.hold_type.manufacturer as string | undefined) ?? ''} ${selectedHold.hold_type.model ?? ''}`.trim(), |
There was a problem hiding this comment.
manufacturer can be an object (per HoldType), but the fallback name construction casts it to string and interpolates it, which can produce "[object Object]" at runtime when manufacturer_ref is missing. Extract the manufacturer name when it is an object (e.g., manufacturer.name) before building the name string.
| ?? `${(selectedHold.hold_type.manufacturer as string | undefined) ?? ''} ${selectedHold.hold_type.model ?? ''}`.trim(), | |
| ?? `${typeof selectedHold.hold_type.manufacturer === 'string' | |
| ? selectedHold.hold_type.manufacturer | |
| : typeof selectedHold.hold_type.manufacturer === 'object' | |
| && selectedHold.hold_type.manufacturer !== null | |
| && 'name' in selectedHold.hold_type.manufacturer | |
| && typeof selectedHold.hold_type.manufacturer.name === 'string' | |
| ? selectedHold.hold_type.manufacturer.name | |
| : ''} ${selectedHold.hold_type.model ?? ''}`.trim(), |
| // Sync batch size with current wall count — React's recommended pattern for resetting | ||
| // state when derived data changes (avoids setState-in-effect anti-pattern). | ||
| if (wallBatchSize !== wallObjects.length) { | ||
| setWallBatchSize(wallObjects.length); | ||
| setWallLoadCount(0); | ||
| setForceLoaded(false); | ||
| } |
There was a problem hiding this comment.
State resets (setWallBatchSize / setWallLoadCount / setForceLoaded) are being triggered during render when wallObjects.length changes. Render-phase state updates are an anti-pattern and can cause extra renders or warnings under StrictMode. Move this reset logic into a useEffect keyed on wallObjects.length (or derive wallBatchSize without storing it) to avoid updating state during render.
| // Sync batch size with current wall count — React's recommended pattern for resetting | |
| // state when derived data changes (avoids setState-in-effect anti-pattern). | |
| if (wallBatchSize !== wallObjects.length) { | |
| setWallBatchSize(wallObjects.length); | |
| setWallLoadCount(0); | |
| setForceLoaded(false); | |
| } | |
| // Reset wall-loading state when the number of wall objects changes. | |
| useEffect(() => { | |
| setWallBatchSize(wallObjects.length); | |
| setWallLoadCount(0); | |
| setForceLoaded(false); | |
| }, [wallObjects.length]); |
| const [deletedHoldTypeIds, setDeletedHoldTypeIds] = useState<Set<string>>(new Set()); | ||
| const [showHolds, setShowHolds] = useState(true); | ||
| const [showVolumes, setShowVolumes] = useState(true); | ||
| const scrollRef = useRef<HTMLDivElement>(null); |
There was a problem hiding this comment.
scrollRef is declared as useRef<HTMLDivElement>(null) but is passed both as the HoldScrollContext value (typed as RefObject<HTMLElement | null>) and assigned to a <section> via a cast. This hides a real type mismatch. Prefer typing the ref as useRef<HTMLElement | null>(null) (or use a div consistently) so you don't need casts and the context/ref stay aligned.
| const scrollRef = useRef<HTMLDivElement>(null); | |
| const scrollRef = useRef<HTMLElement | null>(null); |
No description provided.