Improved drag and drop#1799
Conversation
There was a problem hiding this comment.
Pull request overview
Implements an improved drag-and-drop flow for importing track files into OsmAnd Cloud, including visual drop overlays and folder-targeted dropping from the Tracks UI.
Changes:
- Added a global GPX drag controller + geometry helpers to resolve drop targets (map vs tracks menu folders) and show contextual overlays.
- Refactored Cloud GPX/KMZ/KML import into a shared
useCloudGpxImporthook and reused it from both drag-and-drop and the existing uploader. - Enhanced UX by styling drop targets in the Tracks menu and enabling “zoom to track” after successful upload/open.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| map/src/util/hooks/useCloudGpxImport.js | New shared hook for importing GPX/KMZ/KML into OsmAnd Cloud. |
| map/src/menu/tracks/TracksMenu.jsx | Marks the tracks list container as a drop target (data attribute + styling hook). |
| map/src/menu/tracks/TrackGroupFolder.jsx | Marks opened folders as drop targets for folder-specific imports. |
| map/src/menu/tracks/CloudTrackGroup.jsx | Adds drop-hover styling and folder targeting metadata for groups. |
| map/src/menu/trackfavmenu.module.css | Adds drop-target styles for folders/groups. |
| map/src/map/layers/LocalClientTrackLayer.js | Removes the old map-only drag/drop handling in favor of the global controller. |
| map/src/map/components/GpxMenuDropOverlay.jsx | New overlay for menu-drop guidance (positioned to menu region). |
| map/src/map/components/gpxMapDropOverlay.module.css | Updates overlay styling and renames .overlay → .dropOverlay. |
| map/src/map/components/GpxMapDropOverlay.jsx | Converts overlay to use AppContext drag state + computed insets. |
| map/src/map/components/GpxMapDropGeometry.js | New helper module for computing visible map/menu drop geometry + target resolution. |
| map/src/map/components/GpxFileDragController.jsx | New global window-level drag/drop controller, triggers cloud import into resolved folder. |
| map/src/manager/track/TracksManager.js | Adds IMPORT_FOLDER_NAME constant used for map-drop default folder. |
| map/src/manager/track/SaveTrackManager.js | Sets zoomToTrack when opening a just-uploaded track. |
| map/src/frame/util/CloudGpxUploader.jsx | Refactors uploader to use the shared cloud import hook. |
| map/src/frame/GlobalFrame.js | Mounts the global drag controller and moves main menu open-state to AppContext. |
| map/src/context/AppContext.js | Adds openMainMenu and gpxFileDrag to shared app context. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const reader = new FileReader(); | ||
| reader.addEventListener('load', (e) => { | ||
| const data = e.target.result; | ||
| if (data) { | ||
| mutateUploadedFiles( | ||
| (o) => | ||
| (o[file.name] = { | ||
| file, | ||
| selected, | ||
| data: data, | ||
| name: file.name, | ||
| originalName: file.name, | ||
| folder, | ||
| }) | ||
| ); | ||
| } else { | ||
| ctx.setTrackErrorMsg({ | ||
| title: 'Import error', | ||
| msg: `Unable to import ${file.name}`, | ||
| }); | ||
| ctx.setTrackLoading([...ctx.trackLoading.filter((n) => n !== file.name)]); | ||
| } | ||
| }); |
alisa911
left a comment
There was a problem hiding this comment.
Rename all the new files away from the gpx prefix, the feature handles not only GPX (also KMZ/KML), but only tracks (not favorites). Also move these files out of map/ into frame/, since they're not map-specific.
|
|
||
| const currentCtx = ctxRef.current; | ||
| const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx); | ||
| currentCtx.setGpxFileDrag({ active: true, hoverFolder, overMap }); |
There was a problem hiding this comment.
setGpxFileDrag is called with a fresh object on every dragover, spamming context updates and re-rendering on each mouse move
| const currentCtx = ctxRef.current; | ||
| const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx); | ||
| currentCtx.setGpxFileDrag({ active: true, hoverFolder, overMap }); | ||
| }; |
There was a problem hiding this comment.
the overlay is activated regardless of auth, so free/anonymous users see "Drop GPX to import" but the drop silently no-ops
| } | ||
|
|
||
| const selected = files.length === 1; | ||
| ctx.setTrackLoading(files.map((track) => removeFileExtension(track.name) + GPX_FILE_EXT)); |
There was a problem hiding this comment.
loading entries are set as name + GPX_FILE_EXT but removed by raw file.name on error, so failed .kmz/.kml imports leave a stuck loading placeholder
| import { createTrackFreeName, removeFileExtension, saveTrackToCloud } from '../../manager/track/SaveTrackManager'; | ||
| import { useMutator } from '../Utils'; | ||
|
|
||
| const CLOUD_TRACK_EXTENSIONS = ['.gpx', '.kmz', '.kml']; |
There was a problem hiding this comment.
.kml is accepted but always saved with type: 'GPX' and read as text
| export const DEFAULT_GROUP_NAME = ''; | ||
| export const IMPORT_FOLDER_NAME = 'Import'; |
There was a problem hiding this comment.
IMPORT_FOLDER_NAME = 'Import' is a hardcoded, untranslated cloud-folder name; verify it matches existing folder-naming conventions
| export default function GpxMenuDropOverlay() { | ||
| const ctx = useContext(AppContext); | ||
| const { t } = useTranslation(); | ||
| const ctxRef = useRef(ctx); |
There was a problem hiding this comment.
const ctxRef = useRef(ctx) violates the project rule "refs are a last resort" (CLAUDE.md), and stashing the entire context in a ref is exactly what we don't do.
| const ctx = useContext(AppContext); | ||
| const { t } = useTranslation(); | ||
| const ctxRef = useRef(ctx); | ||
| const [overlayStyle, setOverlayStyle] = useState(null); |
There was a problem hiding this comment.
The hovered folder is already highlighted via the groupDropTarget/folderDropTarget CSS classes; a separately measured portal overlay on top of the same folder duplicates that visual feedback. Pick one mechanism.
| return; | ||
| } | ||
|
|
||
| const update = () => { |
There was a problem hiding this comment.
Locating and measuring the target folder via getElementById to position a fixed overlay couples this to DOM ids and is brittle; prefer rendering the highlight inside the folder component itself.
| return ( | ||
| <> | ||
| <Box | ||
| id={`se-tracks-folder`} | ||
| className={isDropTarget ? styles.folderDropTarget : undefined} | ||
| data-cloud-track-folder={isDropTarget ? group.fullName : undefined} |
There was a problem hiding this comment.
data-cloud-track-folder exists only to bridge the global window drag listener (which has just cursor coords) back to a folder identity via elementFromPoint(...).closest(...). This is DOM-scraping for state React already owns. Attach onDragEnter/onDragOver/onDrop to the folder components (they already close over group.fullName) and set the hover target in context, removing the data-* stamping entirely. If kept short-term, at least extract the attribute name into a shared constant and document the ""-means-root sentinel.
#1283