Combine sidebar workspace options

This commit is contained in:
Neil 2026-05-20 16:25:51 -07:00 committed by GitHub
parent e88b4d1a16
commit 4f2e159f64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 605 additions and 145 deletions

View File

@ -1,49 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Kanban, Plus, SlidersHorizontal } from 'lucide-react'
import { Kanban, Plus } 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,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem
} from '@/components/ui/dropdown-menu'
import type { WorktreeCardProperty } from '../../../../shared/types'
import SidebarFilter from './SidebarFilter'
import SidebarWorkspaceOptionsMenu from './SidebarWorkspaceOptionsMenu'
import WorkspaceKanbanDrawer from './WorkspaceKanbanDrawer'
const GROUP_BY_OPTIONS = [
{ id: 'none', label: 'None' },
{ id: 'workspace-status', label: 'Status' },
{ id: 'pr-status', label: 'PR' },
{ id: 'repo', label: 'Repo' }
] as const
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
// Why: toggles the inline "Agent activity" list rendered below each
// workspace card body (see WorktreeCard -> WorktreeCardAgents). Off hides
// the list; there is no alternate surface.
{ id: 'inline-agents', label: 'Agent activity' }
]
const SORT_OPTIONS = [
{ id: 'name', label: 'Name', description: null },
{
id: 'smart',
label: 'Smart',
description: 'Agents that need attention, then most recent activity.'
},
{ id: 'recent', label: 'Recent', description: null },
{ id: 'repo', label: 'Repo', description: null }
] as const
const isMac = navigator.userAgent.includes('Mac')
const newWorktreeShortcutLabel = isMac ? '⌘N' : 'Ctrl+N'
@ -54,13 +17,6 @@ const SidebarHeader = React.memo(function SidebarHeader() {
const repos = useAppStore((s) => s.repos)
const canCreateWorktree = repos.some((repo) => isGitRepoKind(repo))
const worktreeCardProperties = useAppStore((s) => s.worktreeCardProperties)
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)
const handleWorkspaceBoardOpenChange = useCallback((open: boolean) => {
setWorkspaceBoardOpen(open)
if (!open) {
@ -116,105 +72,10 @@ const SidebarHeader = React.memo(function SidebarHeader() {
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<SidebarFilter preserveWorkspaceBoardOpen onMenuOpenChange={setWorkspaceBoardMenuOpen} />
<DropdownMenu modal={false} onOpenChange={setWorkspaceBoardMenuOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
className="text-muted-foreground"
aria-label="View options"
data-workspace-board-preserve-open=""
>
<SlidersHorizontal className="size-3.5" strokeWidth={2.25} />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
View options
</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="right"
align="start"
sideOffset={8}
className="w-56 pb-2"
data-workspace-board-preserve-open=""
>
<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}
onValueChange={(v) => setSortBy(v as typeof sortBy)}
>
{SORT_OPTIONS.map((opt) => {
const radioItem = (
<DropdownMenuRadioItem
key={opt.id}
value={opt.id}
// Keep the menu open so people can compare sort modes and
// toggle card properties without reopening the same panel.
onSelect={(e) => e.preventDefault()}
>
{opt.label}
</DropdownMenuRadioItem>
)
if (!opt.description) {
return radioItem
}
return (
<Tooltip key={opt.id}>
<TooltipTrigger asChild>{radioItem}</TooltipTrigger>
<TooltipContent side="right" sideOffset={6}>
{opt.description}
</TooltipContent>
</Tooltip>
)
})}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>Show properties</DropdownMenuLabel>
{PROPERTY_OPTIONS.map((opt) => (
<DropdownMenuCheckboxItem
key={opt.id}
checked={worktreeCardProperties.includes(opt.id)}
onCheckedChange={() => toggleWorktreeCardProperty(opt.id)}
onSelect={(e) => e.preventDefault()}
>
{opt.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<SidebarWorkspaceOptionsMenu
preserveWorkspaceBoardOpen
onMenuOpenChange={setWorkspaceBoardMenuOpen}
/>
<Tooltip>
<TooltipTrigger asChild>

View File

@ -0,0 +1,268 @@
import React, { useCallback, useMemo, useState } from 'react'
import { Server, X } from 'lucide-react'
import { useAppStore } from '@/store'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList
} from '@/components/ui/command'
import RepoDotLabel from '@/components/repo/RepoDotLabel'
import { searchRepos } from '@/lib/repo-search'
import type { Repo } from '../../../../shared/types'
function projectCommandFilter(_value: string, search: string, keywords?: string[]): number {
const query = search.trim().toLowerCase()
if (!query) {
return 1
}
const [displayName = '', path = ''] = keywords ?? []
const displayNameIndex = displayName.toLowerCase().indexOf(query)
if (displayNameIndex !== -1) {
return 2 + 1 / (displayNameIndex + 1)
}
const pathIndex = path.toLowerCase().indexOf(query)
if (pathIndex !== -1) {
return 1 + 1 / (pathIndex + 1)
}
return 0
}
const SidebarRepositoryFilterSection = React.memo(function SidebarRepositoryFilterSection() {
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds)
const repos = useAppStore((s) => s.repos)
const [query, setQuery] = useState('')
const [highlightedRepoId, setHighlightedRepoId] = useState('')
const canFilterRepos = repos.length > 1
// Why: derive from current repos so stale ids (e.g. lingering after a repo
// is removed) don't inflate counts or falsely signal an applied filter.
const selectedRepoIdSet = useMemo(() => {
const set = new Set<string>()
for (const repo of repos) {
if (filterRepoIds.includes(repo.id)) {
set.add(repo.id)
}
}
return set
}, [repos, filterRepoIds])
const selectedCount = selectedRepoIdSet.size
const hasRepoFilter = selectedCount > 0
const selectedRepos = useMemo(
() => repos.filter((repo) => selectedRepoIdSet.has(repo.id)),
[repos, selectedRepoIdSet]
)
const availableRepos = useMemo(
() => repos.filter((repo) => !selectedRepoIdSet.has(repo.id)),
[repos, selectedRepoIdSet]
)
const matchingAvailableRepos = useMemo(
() => searchRepos(availableRepos, query),
[availableRepos, query]
)
const handleSelectRepo = useCallback(
(repoId: string) => {
if (!filterRepoIds.includes(repoId)) {
setFilterRepoIds([...filterRepoIds, repoId])
}
setQuery('')
},
[filterRepoIds, setFilterRepoIds]
)
const handleRemoveRepo = useCallback(
(repoId: string) => {
setFilterRepoIds(filterRepoIds.filter((id) => id !== repoId))
},
[filterRepoIds, setFilterRepoIds]
)
const clearRepos = useCallback(() => setFilterRepoIds([]), [setFilterRepoIds])
const handleInputKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
// Why: this command is embedded in a Radix dropdown; text keys should
// stay in the search field instead of triggering menu typeahead.
if (event.key === 'Backspace' && query === '' && selectedRepos.length > 0) {
const lastRepo = selectedRepos.at(-1)
if (lastRepo) {
event.preventDefault()
event.stopPropagation()
handleRemoveRepo(lastRepo.id)
}
return
}
if (event.key === 'Enter') {
const highlightedRepo = availableRepos.find((repo) => repo.id === highlightedRepoId)
const repo = highlightedRepo ?? matchingAvailableRepos[0]
if (repo) {
event.preventDefault()
event.stopPropagation()
handleSelectRepo(repo.id)
}
return
}
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
event.stopPropagation()
}
},
[
availableRepos,
handleRemoveRepo,
handleSelectRepo,
highlightedRepoId,
matchingAvailableRepos,
query,
selectedRepos
]
)
if (!canFilterRepos) {
return null
}
return (
<>
<ProjectFilterHeader
hasRepoFilter={hasRepoFilter}
selectedCount={selectedCount}
onClear={clearRepos}
/>
<Command
filter={projectCommandFilter}
onValueChange={setHighlightedRepoId}
className="bg-transparent"
>
<SelectedProjectPills selectedRepos={selectedRepos} onRemoveRepo={handleRemoveRepo} />
<CommandInput
autoFocus
placeholder={selectedRepos.length > 0 ? 'Add project...' : 'Filter projects...'}
value={query}
onValueChange={setQuery}
onKeyDown={handleInputKeyDown}
className="h-8 py-2 text-xs"
wrapperClassName="mx-1 rounded-[7px] border border-border/70 px-2"
iconClassName="h-3.5 w-3.5"
/>
<CommandList className="max-h-40 py-1">
<CommandEmpty className="py-4 text-[11px]">
{hasRepoFilter ? 'No unselected projects match' : 'No projects match'}
</CommandEmpty>
{availableRepos.map((repo) => (
<CommandItem
key={repo.id}
value={repo.id}
keywords={[repo.displayName, repo.path]}
onSelect={() => handleSelectRepo(repo.id)}
className="mx-1 my-0.5 items-center gap-2 rounded-[7px] px-2 py-1 text-[12px] leading-5 font-medium data-[selected=true]:bg-black/8 dark:data-[selected=true]:bg-white/14"
>
<span className="inline-flex min-w-0 flex-1 items-center gap-1.5">
<RepoDotLabel
name={repo.displayName}
color={repo.badgeColor}
className="max-w-full"
/>
{repo.connectionId && (
<span className="shrink-0 inline-flex items-center gap-0.5 rounded bg-muted px-1 py-0.5 text-[9px] font-medium leading-none text-muted-foreground">
<Server className="size-2.5" />
SSH
</span>
)}
</span>
</CommandItem>
))}
</CommandList>
</Command>
</>
)
})
function SelectedProjectPills({
selectedRepos,
onRemoveRepo
}: {
selectedRepos: Repo[]
onRemoveRepo: (repoId: string) => void
}) {
if (selectedRepos.length === 0) {
return null
}
return (
<div className="scrollbar-sleek mx-1 mb-1 flex max-h-16 flex-wrap gap-1 overflow-y-auto rounded-[7px] border border-border/70 bg-muted/25 p-1">
{selectedRepos.map((repo) => (
<Badge
key={repo.id}
variant="outline"
className="h-5 max-w-full gap-1 border-border/70 bg-background px-1.5 py-0 text-[11px] font-medium"
>
<RepoDotLabel
name={repo.displayName}
color={repo.badgeColor}
className="max-w-[8rem]"
dotClassName="size-1.5"
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={`Remove ${repo.displayName} filter`}
className="-mr-1 size-4 rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
onMouseDown={(event) => event.preventDefault()}
onClick={() => onRemoveRepo(repo.id)}
>
<X className="size-2.5" strokeWidth={2.5} />
</Button>
</Badge>
))}
</div>
)
}
function ProjectFilterHeader({
hasRepoFilter,
selectedCount,
onClear
}: {
hasRepoFilter: boolean
selectedCount: number
onClear: () => void
}) {
return (
<div className="flex items-center justify-between px-2 py-1">
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-muted-foreground">
Projects
{hasRepoFilter && (
<Badge
variant="outline"
className="h-4 min-w-4 px-1 py-0 text-[10px] font-semibold leading-none text-foreground"
>
{selectedCount}
</Badge>
)}
</span>
<button
type="button"
onClick={onClear}
className="rounded-full px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-40 disabled:hover:bg-transparent"
disabled={!hasRepoFilter}
>
Clear
</button>
</div>
)
}
export default SidebarRepositoryFilterSection

View File

@ -0,0 +1,74 @@
import React from 'react'
import { Activity, GitBranch } from 'lucide-react'
import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
const SidebarWorkspaceFilterSection = React.memo(function SidebarWorkspaceFilterSection() {
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
const setShowActiveOnly = useAppStore((s) => s.setShowActiveOnly)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace)
return (
<>
<div className="flex items-center justify-between px-2 py-1">
<span className="text-[11px] font-semibold text-muted-foreground">Filters</span>
</div>
<FilterToggleRow
icon={<Activity className="size-3.5" />}
label="Active only"
checked={showActiveOnly}
onChange={setShowActiveOnly}
/>
<FilterToggleRow
icon={<GitBranch className="size-3.5" />}
label="Hide default branch"
checked={hideDefaultBranchWorkspace}
onChange={setHideDefaultBranchWorkspace}
/>
</>
)
})
function FilterToggleRow({
icon,
label,
checked,
onChange
}: {
icon: React.ReactNode
label: string
checked: boolean
onChange: (next: boolean) => void
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className="flex w-full items-center justify-between gap-2 rounded-[5px] px-2 py-1.5 text-[12px] font-medium hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<span className="inline-flex items-center gap-2 text-foreground">
<span className="text-muted-foreground">{icon}</span>
{label}
</span>
<span
aria-hidden
className={cn(
'relative h-3.5 w-6 shrink-0 rounded-full transition-colors',
checked ? 'bg-primary' : 'bg-muted-foreground/30'
)}
>
<span
className={cn(
'absolute top-0.5 left-0.5 size-2.5 rounded-full bg-background shadow-sm transition-transform',
checked && 'translate-x-2.5'
)}
/>
</span>
</button>
)
}
export default SidebarWorkspaceFilterSection

View File

@ -0,0 +1,257 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { SlidersHorizontal } from 'lucide-react'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import type { WorktreeCardProperty } from '../../../../shared/types'
import SidebarRepositoryFilterSection from './SidebarRepositoryFilterSection'
import SidebarWorkspaceFilterSection from './SidebarWorkspaceFilterSection'
type SidebarWorkspaceOptionsMenuProps = {
preserveWorkspaceBoardOpen?: boolean
onMenuOpenChange?: (open: boolean) => void
}
const GROUP_BY_OPTIONS = [
{ id: 'none', label: 'None' },
{ id: 'workspace-status', label: 'Status' },
{ id: 'pr-status', label: 'PR' },
{ id: 'repo', label: 'Repo' }
] as const
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
// Why: toggles the inline "Agent activity" list rendered below each
// workspace card body (see WorktreeCard -> WorktreeCardAgents). Off hides
// the list; there is no alternate surface.
{ id: 'inline-agents', label: 'Agent activity' }
]
const SORT_OPTIONS = [
{ id: 'name', label: 'Name', description: null },
{
id: 'smart',
label: 'Smart',
description: 'Agents that need attention, then most recent activity.'
},
{ id: 'recent', label: 'Recent', description: null },
{ id: 'repo', label: 'Repo', description: null }
] as const
const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsMenu({
preserveWorkspaceBoardOpen = false,
onMenuOpenChange
}: SidebarWorkspaceOptionsMenuProps) {
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const repos = useAppStore((s) => s.repos)
const worktreeCardProperties = useAppStore((s) => s.worktreeCardProperties)
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)
const [open, setOpen] = useState(false)
const handleOpenChange = useCallback(
(next: boolean) => {
setOpen(next)
onMenuOpenChange?.(next)
},
[onMenuOpenChange]
)
useEffect(() => {
return () => {
onMenuOpenChange?.(false)
}
}, [onMenuOpenChange])
// Why: derive from current repos so stale ids (e.g. lingering after a repo
// is removed) don't inflate counts or falsely signal an applied filter.
const selectedCount = useMemo(() => {
let count = 0
for (const repo of repos) {
if (filterRepoIds.includes(repo.id)) {
count += 1
}
}
return count
}, [repos, filterRepoIds])
const hasRepoFilter = selectedCount > 0
const hasAnyFilter = showActiveOnly || hideDefaultBranchWorkspace || hasRepoFilter
const activeFilterCount =
(showActiveOnly ? 1 : 0) + (hideDefaultBranchWorkspace ? 1 : 0) + selectedCount
const activeFilterLabel = `${activeFilterCount} ${activeFilterCount === 1 ? 'filter' : 'filters'}`
const sortLabel = SORT_OPTIONS.find((opt) => opt.id === sortBy)?.label ?? 'Sort'
const visiblePropertyCount = PROPERTY_OPTIONS.filter((opt) =>
worktreeCardProperties.includes(opt.id)
).length
return (
<DropdownMenu modal={false} open={open} onOpenChange={handleOpenChange}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
type="button"
className="relative text-muted-foreground"
aria-label={
hasAnyFilter
? `Workspace options (${activeFilterLabel} active)`
: 'Workspace options'
}
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
<SlidersHorizontal className="size-3.5" strokeWidth={2.25} />
{hasAnyFilter && (
// Why: this combined options button now owns filtering, so it
// needs the same at-a-glance signal that the old filter button had.
<span
aria-hidden
className="absolute -top-0.5 -right-0.5 flex h-3 min-w-3 items-center justify-center rounded-full bg-primary px-0.5 text-[9px] font-medium leading-none text-primary-foreground"
>
{activeFilterCount > 9 ? '9+' : activeFilterCount}
</span>
)}
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{hasAnyFilter ? `Workspace options (${activeFilterLabel})` : 'Workspace options'}
</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="right"
align="start"
sideOffset={8}
className="w-72 pb-2"
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
<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-stretch"
>
{GROUP_BY_OPTIONS.map((opt) => (
<ToggleGroupItem
key={opt.id}
value={opt.id}
className="h-6 grow basis-0 px-1 text-[10px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
>
{opt.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>Sort by</span>
<span className="text-[11px] font-medium text-muted-foreground">{sortLabel}</span>
</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-44"
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
<DropdownMenuRadioGroup
value={sortBy}
onValueChange={(v) => setSortBy(v as typeof sortBy)}
>
{SORT_OPTIONS.map((opt) => {
const radioItem = (
<DropdownMenuRadioItem
key={opt.id}
value={opt.id}
// Keep the menu open so people can compare sort modes and
// toggle card properties without reopening the same panel.
onSelect={(e) => e.preventDefault()}
>
{opt.label}
</DropdownMenuRadioItem>
)
if (!opt.description) {
return radioItem
}
return (
<Tooltip key={opt.id}>
<TooltipTrigger asChild>{radioItem}</TooltipTrigger>
<TooltipContent side="right" sideOffset={6}>
{opt.description}
</TooltipContent>
</Tooltip>
)
})}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<SidebarWorkspaceFilterSection />
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>Show properties</span>
{visiblePropertyCount > 0 && (
<span className="text-[11px] font-medium text-muted-foreground">
{visiblePropertyCount}
</span>
)}
</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-48"
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
{PROPERTY_OPTIONS.map((opt) => (
<DropdownMenuCheckboxItem
key={opt.id}
checked={worktreeCardProperties.includes(opt.id)}
onCheckedChange={() => toggleWorktreeCardProperty(opt.id)}
onSelect={(e) => e.preventDefault()}
>
{opt.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<SidebarRepositoryFilterSection />
</DropdownMenuContent>
</DropdownMenu>
)
})
export default SidebarWorkspaceOptionsMenu