diff --git a/packages/interface/src/components/Inspector/variants/LocationInspector.tsx b/packages/interface/src/components/Inspector/variants/LocationInspector.tsx index d26e7334ac98..6200d9bca24b 100644 --- a/packages/interface/src/components/Inspector/variants/LocationInspector.tsx +++ b/packages/interface/src/components/Inspector/variants/LocationInspector.tsx @@ -24,6 +24,7 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { useQueryClient } from "@tanstack/react-query"; import { useLocation, useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../../util/navigation"; import { InfoRow, Section, @@ -172,7 +173,8 @@ function OverviewTab({ location }: { location: Location }) { {isOverview && ( { + onClick={(e: React.MouseEvent) => { + if (!shouldNavigate(e)) return; const encodedPath = encodeURIComponent(JSON.stringify(location.sd_path)); navigate(`/explorer?path=${encodedPath}`); }} diff --git a/packages/interface/src/components/JobManager/JobManagerPopover.tsx b/packages/interface/src/components/JobManager/JobManagerPopover.tsx index fcf9808f4e41..78d6c8d70113 100644 --- a/packages/interface/src/components/JobManager/JobManagerPopover.tsx +++ b/packages/interface/src/components/JobManager/JobManagerPopover.tsx @@ -3,6 +3,7 @@ import { Popover, usePopover, TopBarButton } from "@sd/ui"; import clsx from "clsx"; import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../util/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { JobList } from "./components/JobList"; import { useJobsContext } from "./hooks/JobsContext"; @@ -75,7 +76,7 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { {/* Expand to full screen button */} navigate("/jobs")} + onClick={(e: React.MouseEvent) => { if (!shouldNavigate(e)) return; navigate("/jobs"); }} title="Open full jobs screen" /> diff --git a/packages/interface/src/components/JobManager/JobsScreen/index.tsx b/packages/interface/src/components/JobManager/JobsScreen/index.tsx index 7e9a3b092937..d3641c220199 100644 --- a/packages/interface/src/components/JobManager/JobsScreen/index.tsx +++ b/packages/interface/src/components/JobManager/JobsScreen/index.tsx @@ -2,6 +2,7 @@ import { X, FunnelSimple } from "@phosphor-icons/react"; import { TopBarButton } from "@sd/ui"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../../util/navigation"; import { useJobsContext } from "../hooks/JobsContext"; import { JobRow } from "./JobRow"; @@ -58,7 +59,7 @@ export function JobsScreen() { {/* Back button */} navigate(-1)} + onClick={(e: React.MouseEvent) => { if (!shouldNavigate(e)) return; navigate(-1); }} title="Go back" /> diff --git a/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx b/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx index 3a8f3dee0d2d..420a6d7e2c22 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../util/navigation"; import clsx from "clsx"; import type { SpaceItem as SpaceItemType } from "@sd/ts-client"; import { Thumb } from "../../routes/explorer/File/Thumb"; @@ -236,6 +237,7 @@ export function SpaceItem({ // Event handlers const handleClick = (e: React.MouseEvent) => { + if (!shouldNavigate(e)) return; if (effectiveOverrides.onClick) { effectiveOverrides.onClick(e); } else if (path) { diff --git a/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx b/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx index 0a9920be330d..f2a08b75e7ae 100644 --- a/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx @@ -1,6 +1,6 @@ import { Tag as TagIcon, Plus, CaretRight } from '@phosphor-icons/react'; import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { NavLink, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import { useNormalizedQuery, useLibraryMutation } from '../../contexts/SpacedriveContext'; import type { Tag } from '@sd/ts-client'; @@ -20,7 +20,6 @@ interface TagItemProps { } function TagItem({ tag, depth = 0 }: TagItemProps) { - const navigate = useNavigate(); const { loadPreferencesForSpaceItem } = useExplorer(); const [isExpanded, setIsExpanded] = useState(false); @@ -28,17 +27,16 @@ function TagItem({ tag, depth = 0 }: TagItemProps) { const children: Tag[] = []; const hasChildren = children.length > 0; - const handleClick = () => { - loadPreferencesForSpaceItem(`tag:${tag.id}`); - navigate(`/tag/${tag.id}`); - }; - return (
- + {/* Children (recursive) */} {isExpanded && diff --git a/packages/interface/src/components/SpacesSidebar/index.tsx b/packages/interface/src/components/SpacesSidebar/index.tsx index f2f55ce35a96..e50c4ecd421d 100644 --- a/packages/interface/src/components/SpacesSidebar/index.tsx +++ b/packages/interface/src/components/SpacesSidebar/index.tsx @@ -25,6 +25,7 @@ import { useDroppable, useDndContext } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../util/navigation"; // Wrapper that adds a space-level drop zone before each group and makes it sortable function SpaceGroupWithDropZone({ @@ -157,7 +158,7 @@ const SyncButton = memo(function SyncButton() { navigate("/sync")} + onClick={(e: React.MouseEvent) => { if (!shouldNavigate(e)) return; navigate("/sync"); }} title="Open full sync monitor" /> @@ -263,7 +264,7 @@ const JobsButton = memo(function JobsButton({ navigate("/jobs")} + onClick={(e: React.MouseEvent) => { if (!shouldNavigate(e)) return; navigate("/jobs"); }} title="Open full jobs screen" /> diff --git a/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx b/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx index ae1d2558fb46..02aaa3f37380 100644 --- a/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx +++ b/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx @@ -8,6 +8,7 @@ import { Popover, usePopover, TopBarButton } from "@sd/ui"; import clsx from "clsx"; import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { shouldNavigate } from "../../util/navigation"; import { motion } from "framer-motion"; import { PeerList } from "./components/PeerList"; import { ActivityFeed } from "./components/ActivityFeed"; @@ -79,7 +80,7 @@ export function SyncMonitorPopover({ className }: SyncMonitorPopoverProps) { navigate("/sync")} + onClick={(e: React.MouseEvent) => { if (!shouldNavigate(e)) return; navigate("/sync"); }} title="Open full sync monitor" /> diff --git a/packages/interface/src/routes/explorer/Sidebar.tsx b/packages/interface/src/routes/explorer/Sidebar.tsx index 2ff9f95de49a..ae0b8a739d1b 100644 --- a/packages/interface/src/routes/explorer/Sidebar.tsx +++ b/packages/interface/src/routes/explorer/Sidebar.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import { useNavigate, useLocation } from "react-router-dom"; import clsx from "clsx"; import { House, @@ -26,14 +25,10 @@ export function Sidebar() { const client = useSpacedriveClient(); const platform = usePlatform(); const { data: libraries } = useLibraries(); - const navigate = useNavigate(); - const location = useLocation(); const [currentLibraryId, setCurrentLibraryId] = useState( () => client.getCurrentLibraryId(), ); - const isActive = (path: string) => location.pathname === path; - // Listen for library changes from client and update local state useEffect(() => { const handleLibraryChange = (newLibraryId: string) => { @@ -141,25 +136,9 @@ export function Sidebar() {
- navigate("/")} - /> - navigate("/recents")} - /> - navigate("/favorites")} - /> + + +
diff --git a/packages/interface/src/routes/explorer/components/LocationsSection.tsx b/packages/interface/src/routes/explorer/components/LocationsSection.tsx index f82361825c33..5138ee0ab59c 100644 --- a/packages/interface/src/routes/explorer/components/LocationsSection.tsx +++ b/packages/interface/src/routes/explorer/components/LocationsSection.tsx @@ -1,132 +1,119 @@ -import { useNavigate, useParams } from "react-router-dom"; -import { useRef, useEffect } from "react"; -import { Plus } from "@phosphor-icons/react"; -import clsx from "clsx"; -import type { Location } from "@sd/ts-client"; -import { useNormalizedQuery } from "../../../contexts/SpacedriveContext"; -import { Section } from "./Section"; -import { SidebarItem } from "./SidebarItem"; -import { useAddLocationDialog } from "./AddLocationModal"; -import { Location } from "@sd/assets/icons"; -import { useEvent } from "../../../hooks/useEvent"; -import { useDroppable } from "@dnd-kit/core"; +import {useDroppable} from '@dnd-kit/core'; +import {Plus} from '@phosphor-icons/react'; +import {Location} from '@sd/assets/icons'; +import type {Location} from '@sd/ts-client'; +import clsx from 'clsx'; +import {useEffect, useRef} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useNormalizedQuery} from '../../../contexts/SpacedriveContext'; +import {useEvent} from '../../../hooks/useEvent'; +import {useAddLocationDialog} from './AddLocationModal'; +import {Section} from './Section'; +import {SidebarItem} from './SidebarItem'; export function LocationsSection() { - const navigate = useNavigate(); - const { locationId } = useParams(); - const previousLocationIdsRef = useRef>(new Set()); - - const locationsQuery = useNormalizedQuery({ - query: "locations.list", - input: null, - resourceType: "location", - }); - - const locations = locationsQuery.data?.locations || []; - - // Track location IDs to detect new locations - useEffect(() => { - previousLocationIdsRef.current = new Set(locations.map((loc) => loc.id)); - }, [locations]); - - // Listen for new location creation events and navigate to them - useEvent("ResourceChanged", (event) => { - if ("ResourceChanged" in event) { - const { resource_type, resource } = event.ResourceChanged; - - if (resource_type === "location" && typeof resource === "object" && resource !== null) { - const newLocation = resource as Location; - - // Check if this is a new location (not in our previous set) - if (!previousLocationIdsRef.current.has(newLocation.id)) { - navigate(`/location/${newLocation.id}`); - } - } - } - }); - - const handleLocationClick = (location: Location) => { - navigate(`/location/${location.id}`); - }; - - const handleAddLocation = async () => { - // Navigation now happens automatically via ResourceChanged event - await useAddLocationDialog(); - }; - - return ( -
- {locationsQuery.isLoading && ( -
- Loading... -
- )} - - {locationsQuery.error && ( -
- Error: {(locationsQuery.error as Error).message} -
- )} - - {locations.length === 0 && - !locationsQuery.isLoading && - !locationsQuery.error && ( -
- No locations yet -
- )} - - {locations.map((location) => ( - handleLocationClick(location)} - /> - ))} - - -
- ); + const navigate = useNavigate(); + const previousLocationIdsRef = useRef>(new Set()); + + const locationsQuery = useNormalizedQuery({ + query: 'locations.list', + input: null, + resourceType: 'location' + }); + + const locations = locationsQuery.data?.locations || []; + + // Track location IDs to detect new locations + useEffect(() => { + previousLocationIdsRef.current = new Set( + locations.map((loc) => loc.id) + ); + }, [locations]); + + // Listen for new location creation events and navigate to them + useEvent('ResourceChanged', (event) => { + if ('ResourceChanged' in event) { + const {resource_type, resource} = event.ResourceChanged; + + if ( + resource_type === 'location' && + typeof resource === 'object' && + resource !== null + ) { + const newLocation = resource as Location; + + // Check if this is a new location (not in our previous set) + if (!previousLocationIdsRef.current.has(newLocation.id)) { + navigate(`/location/${newLocation.id}`); + } + } + } + }); + + const handleAddLocation = async () => { + // Navigation now happens automatically via ResourceChanged event + await useAddLocationDialog(); + }; + + return ( +
+ {locationsQuery.isLoading && ( +
+ Loading... +
+ )} + + {locationsQuery.error && ( +
+ Error: {(locationsQuery.error as Error).message} +
+ )} + + {locations.length === 0 && + !locationsQuery.isLoading && + !locationsQuery.error && ( +
+ No locations yet +
+ )} + + {locations.map((location) => ( + + ))} + + +
+ ); } // Location item with drop zone support -function LocationDropZone({ - location, - active, - onClick, -}: { - location: Location; - active: boolean; - onClick: () => void; -}) { - const { setNodeRef, isOver } = useDroppable({ - id: `location-drop-${location.id}`, - data: { - action: "move-into", - targetType: "location", - targetId: location.id, - targetPath: location.sd_path, // Use the proper sd_path from the location - }, - }); - - return ( -
- {isOver && ( -
- )} - -
- ); -} \ No newline at end of file +function LocationDropZone({location}: {location: Location}) { + const {setNodeRef, isOver} = useDroppable({ + id: `location-drop-${location.id}`, + data: { + action: 'move-into', + targetType: 'location', + targetId: location.id, + targetPath: location.sd_path // Use the proper sd_path from the location + } + }); + + return ( +
+ {isOver && ( +
+ )} + +
+ ); +} diff --git a/packages/interface/src/routes/explorer/components/SidebarItem.tsx b/packages/interface/src/routes/explorer/components/SidebarItem.tsx index 0f0e433f4d5e..ba2319a8887c 100644 --- a/packages/interface/src/routes/explorer/components/SidebarItem.tsx +++ b/packages/interface/src/routes/explorer/components/SidebarItem.tsx @@ -1,39 +1,48 @@ import clsx from "clsx"; +import { NavLink } from "react-router-dom"; -interface SidebarItemProps { +interface SidebarItemBaseProps { icon: React.ElementType | string; label: string; - active?: boolean; weight?: "regular" | "fill" | "bold"; color?: string; - onClick?: () => void; className?: string; } -export function SidebarItem({ +interface SidebarItemLinkProps extends SidebarItemBaseProps { + to: string; + end?: boolean; + onClick?: (e: React.MouseEvent) => void; + active?: never; +} + +interface SidebarItemButtonProps extends SidebarItemBaseProps { + to?: never; + end?: never; + onClick?: (e: React.MouseEvent) => void; + active?: boolean; +} + +type SidebarItemProps = SidebarItemLinkProps | SidebarItemButtonProps; + +function SidebarItemContent({ icon, label, - active, + isActive, weight = "bold", color, - onClick, - className, -}: SidebarItemProps) { +}: { + icon: React.ElementType | string; + label: string; + isActive: boolean; + weight?: "regular" | "fill" | "bold"; + color?: string; +}) { const isImageUrl = typeof icon === "string"; const Icon = isImageUrl ? null : icon; return ( - ); -} \ No newline at end of file +} diff --git a/packages/interface/src/util/navigation.ts b/packages/interface/src/util/navigation.ts new file mode 100644 index 000000000000..cca1bf2c5c58 --- /dev/null +++ b/packages/interface/src/util/navigation.ts @@ -0,0 +1,5 @@ +export function shouldNavigate(e: React.MouseEvent): boolean { + if (e.button !== 0) return false; + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return false; + return true; +}