mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
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:
@@ -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
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user