mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(planner): keep the file-upload callback above the place bail-out (#1800)
PlaceInspector returns null when no place is selected, but the useCallback for handleFileUpload sat below that return. Clearing the selection therefore changed the hook count and React threw "Rendered fewer hooks than expected", which takes the whole tree down since nothing catches it. Move the callback above the bail-out and read the id through place?.id.
This commit is contained in:
@@ -1280,4 +1280,18 @@ describe('PlaceInspector', () => {
|
||||
await waitFor(() => expect(onUploadImage).toHaveBeenCalledWith(place.id, file));
|
||||
});
|
||||
|
||||
it('FE-PLANNER-INSPECTOR-098: deselecting and reselecting a place survives a rerender', async () => {
|
||||
// The component bails out with `if (!place) return null`. Any hook below that
|
||||
// line runs only while a place is selected, so clearing the selection changes
|
||||
// the hook count and React tears the whole tree down.
|
||||
const { rerender } = render(<PlaceInspector {...defaultProps} />);
|
||||
expect(screen.getByText('Eiffel Tower')).toBeTruthy();
|
||||
|
||||
rerender(<PlaceInspector {...defaultProps} place={null} />);
|
||||
expect(screen.queryByText('Eiffel Tower')).toBeNull();
|
||||
|
||||
rerender(<PlaceInspector {...defaultProps} />);
|
||||
expect(screen.getByText('Eiffel Tower')).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -231,6 +231,31 @@ export default function PlaceInspector({
|
||||
})
|
||||
}, [place, openSavePicker])
|
||||
|
||||
// Sits above the `if (!place)` bail-out below: a hook after an early return is
|
||||
// only reached while a place is selected, so deselecting one mid-session
|
||||
// changes the hook count and React tears the tree down.
|
||||
const placeId = place?.id
|
||||
const handleFileUpload = useCallback(async (e) => {
|
||||
const selectedFiles = Array.from((e.target as HTMLInputElement).files || [])
|
||||
if (!selectedFiles.length || !onFileUpload || !placeId) return
|
||||
setIsUploading(true)
|
||||
try {
|
||||
for (const file of selectedFiles) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('place_id', String(placeId))
|
||||
await onFileUpload(fd)
|
||||
}
|
||||
setFilesExpanded(true)
|
||||
} catch (err: unknown) {
|
||||
console.error('Upload failed', err)
|
||||
toast.error(translateApiError(t, err, 'files.uploadError'))
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
}, [onFileUpload, placeId, toast, t])
|
||||
|
||||
const startNameEdit = () => {
|
||||
if (!onUpdatePlace) return
|
||||
setNameValue(place.name || '')
|
||||
@@ -283,27 +308,6 @@ export default function PlaceInspector({
|
||||
|
||||
const placeFiles = (files || []).filter(f => String(f.place_id) === String(place.id) || (f.linked_place_ids || []).includes(place.id))
|
||||
|
||||
const handleFileUpload = useCallback(async (e) => {
|
||||
const selectedFiles = Array.from((e.target as HTMLInputElement).files || [])
|
||||
if (!selectedFiles.length || !onFileUpload) return
|
||||
setIsUploading(true)
|
||||
try {
|
||||
for (const file of selectedFiles) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('place_id', String(place.id))
|
||||
await onFileUpload(fd)
|
||||
}
|
||||
setFilesExpanded(true)
|
||||
} catch (err: unknown) {
|
||||
console.error('Upload failed', err)
|
||||
toast.error(translateApiError(t, err, 'files.uploadError'))
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
}, [onFileUpload, place.id, toast, t])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user