From 1cfd5ea8c47177f45db4c53420e2527292365436 Mon Sep 17 00:00:00 2001 From: Martin Laporte Date: Thu, 1 Oct 2026 10:51:25 +0200 Subject: [PATCH] Simpler sorting of worktrees --- bun.lock | 5 +- packages/treebeard/package.json | 1 - packages/treebeard/src/App.test.tsx | 1 - packages/treebeard/src/App.tsx | 27 +--- .../src/components/RepoDashboard.test.tsx | 35 +---- .../src/components/RepoDashboard.tsx | 124 ++++-------------- .../treebeard/src/hooks/useConfig.test.ts | 4 - packages/treebeard/src/hooks/useConfig.ts | 9 -- 8 files changed, 36 insertions(+), 170 deletions(-) diff --git a/bun.lock b/bun.lock index 2837685..9b6a3f4 100644 --- a/bun.lock +++ b/bun.lock @@ -12,10 +12,9 @@ }, "packages/treebeard": { "name": "treebeard", - "version": "2.17.1", + "version": "2.19.0", "dependencies": { "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@mantine/core": "^7.17.0", "@mantine/hooks": "^7.17.0", @@ -99,8 +98,6 @@ "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], - "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], - "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], diff --git a/packages/treebeard/package.json b/packages/treebeard/package.json index fc30edf..0e8768d 100644 --- a/packages/treebeard/package.json +++ b/packages/treebeard/package.json @@ -14,7 +14,6 @@ }, "dependencies": { "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@mantine/core": "^7.17.0", "@mantine/hooks": "^7.17.0", diff --git a/packages/treebeard/src/App.test.tsx b/packages/treebeard/src/App.test.tsx index 58308d8..a299601 100644 --- a/packages/treebeard/src/App.test.tsx +++ b/packages/treebeard/src/App.test.tsx @@ -82,7 +82,6 @@ describe('App', () => { setPollInterval: vi.fn(async () => {}), setAutoUpdateEnabled: vi.fn(async () => {}), setUpdateCheckInterval: vi.fn(async () => {}), - reorderRepos: vi.fn(async () => {}), setDefaultIde: vi.fn(async () => {}) }) }) diff --git a/packages/treebeard/src/App.tsx b/packages/treebeard/src/App.tsx index 5da81fe..336d936 100644 --- a/packages/treebeard/src/App.tsx +++ b/packages/treebeard/src/App.tsx @@ -23,7 +23,6 @@ import { useSensor, useSensors } from '@dnd-kit/core' -import { arrayMove } from '@dnd-kit/sortable' import { RepoDashboard } from './components/RepoDashboard' import { WorkspaceDashboard } from './components/WorkspaceDashboard' import { SettingsModal } from './components/SettingsModal' @@ -34,7 +33,7 @@ import { useJiraDrag } from './hooks/useJiraDrag' import { rpc } from './rpc' import { WORKTREE_DRAG_PREFIX, WORKSPACE_TARGET_PREFIX } from './shared/workspace-dnd' import type { DragEndEvent } from '@dnd-kit/core' -import type { DependencyStatus, RepoConfig } from './shared/types' +import type { DependencyStatus } from './shared/types' import type { JiraIssueDragData } from './components/JiraIssueCard' // Neon-blue palette tuned for dark backgrounds @@ -80,7 +79,6 @@ export default function App() { setPollInterval, setAutoUpdateEnabled, setUpdateCheckInterval, - reorderRepos, setDefaultIde, setRepoSetupCommands, setJiraPanelOpen, @@ -90,7 +88,6 @@ export default function App() { const [search, setSearch] = useState('') const [dependencyStatus, setDependencyStatus] = useState(null) const [jiraDropTargets, setJiraDropTargets] = useState>({}) - const [orderedRepos, setOrderedRepos] = useState([]) const [panelWidth, setPanelWidth] = useState(260) const [workspaceAttachError, setWorkspaceAttachError] = useState(null) @@ -104,11 +101,6 @@ export default function App() { const { issues: jiraIssues, loading: jiraLoading, refresh: refreshJira } = useMyJiraIssues(pollIntervalSec) - // Keep ordered repos in sync with config - useEffect(() => { - if (config) setOrderedRepos(config.repositories) - }, [config]) - // Native mouse drag for Jira issues — works across AppShell panels const handleJiraDrop = useCallback((repoId: string, data: JiraIssueDragData) => { setJiraDropTargets((prev) => ({ ...prev, [repoId]: `${data.issueKey}-` })) @@ -122,7 +114,7 @@ export default function App() { const { isDragging: isDraggingJira, draggingKey, overRepoId, onMouseDown: onIssueMouseDown } = useJiraDrag(handleJiraDrop) - // @dnd-kit coordinates repository reordering and worktree-to-workspace links. + // @dnd-kit coordinates worktree-to-workspace links. const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) const handleWorkspaceAttach = useCallback(async (workspaceId: string, repoId: string, worktreePath: string) => { @@ -154,16 +146,8 @@ export default function App() { source.slice(separator + 1) ) } - return } - if (active.id === over.id) return - const oldIndex = orderedRepos.findIndex((r) => r.id === active.id) - const newIndex = orderedRepos.findIndex((r) => r.id === over.id) - if (oldIndex === -1 || newIndex === -1) return - const reordered = arrayMove(orderedRepos, oldIndex, newIndex) - setOrderedRepos(reordered) - void reorderRepos(reordered) - }, [handleWorkspaceAttach, orderedRepos, reorderRepos]) + }, [handleWorkspaceAttach]) const loadDependencies = useCallback(async () => { try { @@ -308,7 +292,7 @@ export default function App() { Manage worktrees across all configured repositories. { setOrderedRepos(repos); void reorderRepos(repos) }} isDraggingJira={isDraggingJira} overRepoId={overRepoId} jiraDropTargets={jiraDropTargets} diff --git a/packages/treebeard/src/components/RepoDashboard.test.tsx b/packages/treebeard/src/components/RepoDashboard.test.tsx index cbcf4cd..6819490 100644 --- a/packages/treebeard/src/components/RepoDashboard.test.tsx +++ b/packages/treebeard/src/components/RepoDashboard.test.tsx @@ -36,25 +36,6 @@ vi.mock('@dnd-kit/core', () => ({ DragOverlay: ({ children }: { children: ReactNode }) =>
{children}
})) -vi.mock('@dnd-kit/sortable', () => ({ - SortableContext: ({ children }: { children: ReactNode }) =>
{children}
, - useSortable: () => ({ - attributes: {}, - listeners: {}, - setNodeRef: () => {}, - transform: null, - transition: null, - isDragging: false - }), - verticalListSortingStrategy: {}, - arrayMove: (array: T[], from: number, to: number) => { - const copy = [...array] - const [item] = copy.splice(from, 1) - copy.splice(to, 0, item) - return copy - } -})) - vi.mock('@dnd-kit/utilities', () => ({ CSS: { Transform: { @@ -157,7 +138,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={''} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -197,7 +177,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={'feat'} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -216,7 +195,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={'missing'} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -227,7 +205,7 @@ describe('RepoDashboard', () => { expect(screen.queryByText('treebeard')).toBeNull() }) - it('renders active repositories before inactive repositories without showing main worktrees by default', async () => { + it('renders repositories alphabetically regardless of activity without showing main worktrees by default', async () => { const repos: RepoConfig[] = [ { id: 'repo-1', name: 'repo-one', path: '/repo-one' }, { id: 'repo-2', name: 'repo-two', path: '/repo-two' }, @@ -270,7 +248,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={''} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -280,10 +257,10 @@ describe('RepoDashboard', () => { await waitFor(() => { const names = screen.getAllByRole('heading', { level: 4 }).map((heading) => heading.textContent) - expect(names).toEqual(['repo-two', 'repo-three', 'repo-one', 'repo-four']) + expect(names).toEqual(['repo-four', 'repo-one', 'repo-three', 'repo-two']) }) - expect(screen.getAllByTestId('worktree-card').map((card) => card.textContent)).toEqual(['feat/two', 'feat/three']) + expect(screen.getAllByTestId('worktree-card').map((card) => card.textContent)).toEqual(['feat/three', 'feat/two']) }) it('shows main status and action controls for inactive repositories without rendering a main card body', () => { @@ -314,7 +291,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={''} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -357,7 +333,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search="" defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -379,7 +354,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search="" defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -424,7 +398,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search="" defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -471,7 +444,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={''} defaultIde="intellij" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} @@ -512,7 +484,6 @@ describe('RepoDashboard', () => { fetchIntervalSec={300} search={'main'} defaultIde="vscode" - onReorder={() => {}} isDraggingJira={false} overRepoId={null} jiraDropTargets={{}} diff --git a/packages/treebeard/src/components/RepoDashboard.tsx b/packages/treebeard/src/components/RepoDashboard.tsx index 0734961..1714ec8 100644 --- a/packages/treebeard/src/components/RepoDashboard.tsx +++ b/packages/treebeard/src/components/RepoDashboard.tsx @@ -1,11 +1,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { Stack, Group, Title, Text, ActionIcon, Loader, Alert, Collapse, Code } from '@mantine/core' -import { IconRefresh, IconPlus, IconChevronDown, IconChevronRight, IconGripVertical, IconAlertCircle, IconCheck, IconX } from '@tabler/icons-react' -import { - SortableContext, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable' +import { IconRefresh, IconPlus, IconChevronDown, IconChevronRight, IconAlertCircle, IconCheck, IconX } from '@tabler/icons-react' import { useDraggable } from '@dnd-kit/core' import { CSS } from '@dnd-kit/utilities' import { WorktreeCard } from './WorktreeCard' @@ -20,8 +15,6 @@ import { useWorktreeStatus } from '../hooks/useWorktreeStatus' import { WORKTREE_DRAG_PREFIX } from '../shared/workspace-dnd' import type { IdeId, RepoConfig, Worktree } from '../shared/types' -type RepoActivity = 'active' | 'inactive' | 'unknown' - // --- RepoSection --- interface MainWorktreeControlsProps { @@ -107,7 +100,6 @@ interface RepoSectionProps { isDropTarget: boolean isOver: boolean jiraDropBranch: string | null - onActivityChange: (repoId: string, activity: RepoActivity) => void onJiraDropBranchClear: () => void } @@ -123,13 +115,11 @@ function RepoSection({ isDropTarget, isOver, jiraDropBranch, - onActivityChange, onJiraDropBranchClear }: RepoSectionProps) { - const { worktrees, loading, loaded, error, deleteError, deletingPaths, startDelete, clearDeleteError, settingUpPaths, setupError, startSetup, clearSetupError, refresh } = useWorktrees(repo.path, pollIntervalSec) + const { worktrees, loading, error, deleteError, deletingPaths, startDelete, clearDeleteError, settingUpPaths, setupError, startSetup, clearSetupError, refresh } = useWorktrees(repo.path, pollIntervalSec) const [addOpened, setAddOpened] = useState(false) const [refreshKey, setRefreshKey] = useState(0) - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: repo.id }) const { shortenPath } = useHomedir() const handleFetched = useCallback(() => setRefreshKey((k) => k + 1), []) @@ -163,26 +153,15 @@ function RepoSection({ ) : worktrees.filter((wt) => !wt.isMain) const mainWorktree = worktrees.find((wt) => wt.isMain) - const hasActiveWorktrees = worktrees.some((wt) => !wt.isMain) - const activity: RepoActivity = loaded && !error - ? hasActiveWorktrees ? 'active' : 'inactive' - : 'unknown' const shouldShowBody = loading || Boolean(error) || Boolean(deleteError) || Boolean(setupError) || visibleWorktrees.length > 0 - useEffect(() => { - onActivityChange(repo.id, activity) - }, [repo.id, activity, onActivityChange]) - if (!loading && visibleWorktrees.length === 0 && query) return null return (
- - - {shouldShowBody && ( {isCollapsed ? : } @@ -332,7 +301,6 @@ interface RepoDashboardProps { fetchIntervalSec: number search: string defaultIde: IdeId - onReorder: (repos: RepoConfig[]) => void // Jira drag state from native drag (useJiraDrag) isDraggingJira: boolean overRepoId: string | null @@ -340,60 +308,25 @@ interface RepoDashboardProps { onJiraDropBranchClear: (repoId: string) => void } -function activityRank(activity: RepoActivity | undefined): number { - if (activity === 'active') return 0 - if (activity === 'inactive') return 2 - return 1 -} - export function RepoDashboard({ repos, pollIntervalSec, fetchIntervalSec, search, defaultIde, - onReorder, isDraggingJira, overRepoId, jiraDropTargets, onJiraDropBranchClear }: RepoDashboardProps) { const { collapsed, toggle, expand } = useCollapsed() - const [orderedRepos, setOrderedRepos] = useState(repos) - const [repoActivityById, setRepoActivityById] = useState>({}) - useEffect(() => { - setOrderedRepos(repos) - }, [repos]) - - useEffect(() => { - setRepoActivityById((prev) => { - const repoIds = new Set(repos.map((repo) => repo.id)) - const next: Record = {} - for (const [repoId, activity] of Object.entries(prev)) { - if (repoIds.has(repoId)) next[repoId] = activity - } - return Object.keys(next).length === Object.keys(prev).length ? prev : next - }) - }, [repos]) - - const handleActivityChange = useCallback((repoId: string, activity: RepoActivity) => { - setRepoActivityById((prev) => { - if (prev[repoId] === activity) return prev - return { ...prev, [repoId]: activity } - }) - }, []) - - const displayRepos = useMemo(() => { - const orderById = new Map(orderedRepos.map((repo, index) => [repo.id, index])) - return [...orderedRepos].sort((a, b) => { - const rankDiff = activityRank(repoActivityById[a.id]) - activityRank(repoActivityById[b.id]) - if (rankDiff !== 0) return rankDiff - return (orderById.get(a.id) ?? 0) - (orderById.get(b.id) ?? 0) - }) - }, [orderedRepos, repoActivityById]) + const sortedRepos = useMemo( + () => [...repos].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })), + [repos] + ) - if (orderedRepos.length === 0) { + if (sortedRepos.length === 0) { return ( No repositories configured @@ -403,27 +336,24 @@ export function RepoDashboard({ } return ( - r.id)} strategy={verticalListSortingStrategy}> - - {displayRepos.map((repo) => ( - toggle(repo.id)} - onExpand={() => expand(repo.id)} - isDropTarget={isDraggingJira} - isOver={overRepoId === repo.id} - jiraDropBranch={jiraDropTargets[repo.id] ?? null} - onActivityChange={handleActivityChange} - onJiraDropBranchClear={() => onJiraDropBranchClear(repo.id)} - /> - ))} - - + + {sortedRepos.map((repo) => ( + toggle(repo.id)} + onExpand={() => expand(repo.id)} + isDropTarget={isDraggingJira} + isOver={overRepoId === repo.id} + jiraDropBranch={jiraDropTargets[repo.id] ?? null} + onJiraDropBranchClear={() => onJiraDropBranchClear(repo.id)} + /> + ))} + ) } diff --git a/packages/treebeard/src/hooks/useConfig.test.ts b/packages/treebeard/src/hooks/useConfig.test.ts index 6f96414..53d67b3 100644 --- a/packages/treebeard/src/hooks/useConfig.test.ts +++ b/packages/treebeard/src/hooks/useConfig.test.ts @@ -122,7 +122,6 @@ describe('useConfig', () => { await result.current.setPollInterval(120) await result.current.setAutoUpdateEnabled(false) await result.current.setUpdateCheckInterval(45) - await result.current.reorderRepos([initial.repositories[1], initial.repositories[0]]) }) expect(setConfigRequest).toHaveBeenNthCalledWith(1, { @@ -134,8 +133,5 @@ describe('useConfig', () => { expect(setConfigRequest).toHaveBeenNthCalledWith(3, { config: { ...initial, updateCheckIntervalMin: 45 } }) - expect(setConfigRequest).toHaveBeenNthCalledWith(4, { - config: { ...initial, repositories: [initial.repositories[1], initial.repositories[0]] } - }) }) }) diff --git a/packages/treebeard/src/hooks/useConfig.ts b/packages/treebeard/src/hooks/useConfig.ts index a5708eb..df21e04 100644 --- a/packages/treebeard/src/hooks/useConfig.ts +++ b/packages/treebeard/src/hooks/useConfig.ts @@ -77,14 +77,6 @@ export function useConfig() { [config, save] ) - const reorderRepos = useCallback( - async (repositories: RepoConfig[]) => { - if (!config) return - await save({ ...config, repositories }) - }, - [config, save] - ) - const setDefaultIde = useCallback( async (ide: IdeId) => { if (!config) return @@ -132,7 +124,6 @@ export function useConfig() { setPollInterval, setAutoUpdateEnabled, setUpdateCheckInterval, - reorderRepos, setDefaultIde, setRepoSetupCommands, setJiraPanelOpen,