feat(sidebar): consolidate group-by + filter controls (#818)

* feat(sidebar): move group-by control into view options menu

Consolidates the standalone Group by segmented control into the view
options dropdown alongside Sort by and Show properties, and opens the
menu to the right of the trigger so it no longer covers the worktree
list.

* feat(sidebar): combine active + repo filter into single filter control

Replaces the separate Active toggle and repo dropdown trigger in the
search bar with a single ListFilter icon button that opens a unified
dropdown covering Status (Active only) and Repositories. The button
expands with an inline summary and accent background when any filter
is applied, making the active state obvious, and exposes a Clear
filters action.
This commit is contained in:
Neil
2026-04-18 22:53:23 -07:00
committed by GitHub
parent 6543e3dd3d
commit d0ef5fe4ec
4 changed files with 147 additions and 133 deletions
@@ -1,46 +0,0 @@
import React from 'react'
import { useAppStore } from '@/store'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
const GroupControls = React.memo(function GroupControls() {
const groupBy = useAppStore((s) => s.groupBy)
const setGroupBy = useAppStore((s) => s.setGroupBy)
return (
<div className="flex items-center justify-between px-2 pb-1.5 gap-1">
<ToggleGroup
type="single"
value={groupBy}
onValueChange={(v) => {
if (v) {
setGroupBy(v as typeof groupBy)
}
}}
variant="outline"
size="sm"
className="h-6 flex-1 justify-start"
>
<ToggleGroupItem
value="none"
className="h-6 px-2 text-[10px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
>
All
</ToggleGroupItem>
<ToggleGroupItem
value="pr-status"
className="h-6 px-2 text-[10px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
>
PR Status
</ToggleGroupItem>
<ToggleGroupItem
value="repo"
className="h-6 px-2 text-[10px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
>
Repo
</ToggleGroupItem>
</ToggleGroup>
</div>
)
})
export default GroupControls
+110 -84
View File
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react'
import { Search, X, Activity, FolderTree, FolderPlus } from 'lucide-react'
import { Search, X, Activity, ListFilter, FolderPlus } from 'lucide-react'
import { useAppStore } from '@/store'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
@@ -8,6 +8,7 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
@@ -37,30 +38,39 @@ const SearchBar = React.memo(function SearchBar() {
[filterRepoIds, setFilterRepoIds]
)
const repoTriggerLabel =
selectedRepos.length === 0 ? (
<span className="flex items-center gap-1">
<FolderTree className="size-3 text-muted-foreground" />
<span>All</span>
</span>
) : selectedRepos.length === 1 ? (
<RepoDotLabel
name={selectedRepos[0].displayName}
color={selectedRepos[0].badgeColor}
dotClassName="size-1"
/>
) : (
<span className="flex items-center gap-1">
<FolderTree className="size-3 text-muted-foreground" />
<span>{selectedRepos.length} repos</span>
</span>
)
const handleClear = useCallback(() => setSearchQuery(''), [setSearchQuery])
const handleToggleActive = useCallback(
() => setShowActiveOnly(!showActiveOnly),
[showActiveOnly, setShowActiveOnly]
)
const canFilterRepos = repos.length > 1
const hasRepoFilter = canFilterRepos && selectedRepos.length > 0
const hasAnyFilter = showActiveOnly || hasRepoFilter
const activeFilterCount = (showActiveOnly ? 1 : 0) + (hasRepoFilter ? selectedRepos.length : 0)
const filterSummary = (() => {
if (!hasAnyFilter) {
return null
}
if (showActiveOnly && !hasRepoFilter) {
return (
<span className="flex items-center gap-1">
<Activity className="size-3" />
<span>Active</span>
</span>
)
}
if (!showActiveOnly && hasRepoFilter && selectedRepos.length === 1) {
return (
<RepoDotLabel
name={selectedRepos[0].displayName}
color={selectedRepos[0].badgeColor}
dotClassName="size-1"
/>
)
}
return <span>{activeFilterCount} filters</span>
})()
return (
<div className="px-2 pb-4">
@@ -78,71 +88,87 @@ const SearchBar = React.memo(function SearchBar() {
<X className="size-3" />
</Button>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={handleToggleActive}
className={cn(
'relative size-5',
showActiveOnly && 'bg-accent text-accent-foreground'
)}
>
<Activity className="size-3" />
{showActiveOnly ? (
<span className="absolute top-0.5 right-0.5 size-1.5 rounded-full bg-green-500 ring-1 ring-background" />
) : null}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{showActiveOnly ? 'Show all' : 'Active only'}
</TooltipContent>
</Tooltip>
{repos.length > 1 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
type="button"
aria-label="Filter repositories"
className="h-5 w-auto gap-1 border-none bg-transparent px-1 text-[10px] font-normal shadow-none hover:bg-accent/60 focus-visible:ring-0"
>
{repoTriggerLabel}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onSelect={() => {
setFilterRepoIds([])
}}
>
All repos
</DropdownMenuItem>
{repos.map((r) => (
<DropdownMenuCheckboxItem
key={r.id}
checked={filterRepoIds.includes(r.id)}
onCheckedChange={() => handleToggleRepo(r.id)}
onSelect={(event) => event.preventDefault()}
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
type="button"
aria-label="Filter worktrees"
className={cn(
'h-5 gap-1 border-none px-1 text-[10px] font-normal shadow-none focus-visible:ring-0',
hasAnyFilter
? 'w-auto bg-accent text-accent-foreground hover:bg-accent/80'
: 'size-5 w-5 bg-transparent hover:bg-accent/60'
)}
>
<RepoDotLabel name={r.displayName} color={r.badgeColor} />
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
inset
onSelect={() => {
addRepo()
}}
>
<FolderPlus className="absolute left-2.5 size-3.5 text-muted-foreground" />
Add repo
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<ListFilter className="size-3 shrink-0" />
{filterSummary}
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{hasAnyFilter ? 'Edit filters' : 'Filter worktrees'}
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[12rem]">
<DropdownMenuLabel>Status</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={showActiveOnly}
onCheckedChange={handleToggleActive}
onSelect={(event) => event.preventDefault()}
>
<Activity className="size-3.5 text-muted-foreground" />
Active only
</DropdownMenuCheckboxItem>
{canFilterRepos && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Repositories</DropdownMenuLabel>
{repos.map((r) => (
<DropdownMenuCheckboxItem
key={r.id}
checked={filterRepoIds.includes(r.id)}
onCheckedChange={() => handleToggleRepo(r.id)}
onSelect={(event) => event.preventDefault()}
>
<RepoDotLabel name={r.displayName} color={r.badgeColor} />
</DropdownMenuCheckboxItem>
))}
</>
)}
{hasAnyFilter && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
setShowActiveOnly(false)
setFilterRepoIds([])
}}
>
<X className="size-3.5 text-muted-foreground" />
Clear filters
</DropdownMenuItem>
</>
)}
{canFilterRepos && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
inset
onSelect={() => {
addRepo()
}}
>
<FolderPlus className="absolute left-2.5 size-3.5 text-muted-foreground" />
Add repo
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
@@ -3,6 +3,7 @@ import { Plus, SlidersHorizontal } from 'lucide-react'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { isGitRepoKind } from '../../../../shared/repo-kind'
import {
DropdownMenu,
@@ -16,6 +17,12 @@ import {
} from '@/components/ui/dropdown-menu'
import type { WorktreeCardProperty } from '../../../../shared/types'
const GROUP_BY_OPTIONS = [
{ id: 'none', label: 'All' },
{ id: 'pr-status', label: 'PR Status' },
{ id: 'repo', label: 'Repo' }
] as const
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
{ id: 'status', label: 'Terminal status' },
{ id: 'unread', label: 'Unread indicator' },
@@ -44,6 +51,8 @@ const SidebarHeader = React.memo(function SidebarHeader() {
const toggleWorktreeCardProperty = useAppStore((s) => s.toggleWorktreeCardProperty)
const sortBy = useAppStore((s) => s.sortBy)
const setSortBy = useAppStore((s) => s.setSortBy)
const groupBy = useAppStore((s) => s.groupBy)
const setGroupBy = useAppStore((s) => s.setGroupBy)
return (
<div className="flex h-8 items-center justify-between px-4 mt-1">
@@ -69,7 +78,34 @@ const SidebarHeader = React.memo(function SidebarHeader() {
View options
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-56 pb-2">
<DropdownMenuContent side="right" align="start" sideOffset={8} className="w-56 pb-2">
<DropdownMenuLabel>Group by</DropdownMenuLabel>
<div className="px-2 pt-0.5 pb-1">
<ToggleGroup
type="single"
value={groupBy}
onValueChange={(v) => {
if (v) {
setGroupBy(v as typeof groupBy)
}
}}
variant="outline"
size="sm"
className="h-6 w-full justify-start"
>
{GROUP_BY_OPTIONS.map((opt) => (
<ToggleGroupItem
key={opt.id}
value={opt.id}
className="h-6 px-2 text-[10px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
>
{opt.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<DropdownMenuSeparator />
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={sortBy}
@@ -5,7 +5,6 @@ import { useSidebarResize } from '@/hooks/useSidebarResize'
import SidebarHeader from './SidebarHeader'
import SidebarNav from './SidebarNav'
import SearchBar from './SearchBar'
import GroupControls from './GroupControls'
import WorktreeList from './WorktreeList'
import SidebarToolbar from './SidebarToolbar'
import WorktreeMetaDialog from './WorktreeMetaDialog'
@@ -51,7 +50,6 @@ function Sidebar(): React.JSX.Element {
<SidebarNav />
<SidebarHeader />
<SearchBar />
<GroupControls />
{/* Virtualized scrollable list */}
<WorktreeList />