fix(sidebar): add project sort modes (#2067)

* fix(sidebar): add project sort modes

* fix(sidebar): preserve manual sort order

* fix(sidebar): move sort state before render

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
Leonid
2026-07-11 14:43:57 +03:00
committed by GitHub
co-authored by bashrusakh
parent f6326e1c35
commit bfaf62e222
16 changed files with 256 additions and 18 deletions
@@ -68,6 +68,8 @@ export function SidebarHeader(props: Props): React.ReactNode {
const setDisplayMode = useSessionDisplayStore((state) => state.setDisplayMode);
const toggleRecentSection = useSessionDisplayStore((state) => state.toggleRecentSection);
const toggleArchivedSessions = useSessionDisplayStore((state) => state.toggleArchivedSessions);
const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder);
const setProjectSortOrder = useSessionDisplayStore((state) => state.setProjectSortOrder);
// VS Code forces the expanded layout, so the mode toggle is meaningless there.
const showDisplayModeToggle = !isVSCodeRuntime();
@@ -175,6 +177,60 @@ export function SidebarHeader(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.sortProjects')}
>
<Icon name="arrow-up-double" className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.sortProjects')}</p></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[160px]">
<DropdownMenuItem
onClick={() => setProjectSortOrder('manual')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.projectSort.manual')}</span>
{projectSortOrder === 'manual' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setProjectSortOrder('a-z')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.projectSort.aToZ')}</span>
{projectSortOrder === 'a-z' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setProjectSortOrder('z-a')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.projectSort.zToA')}</span>
{projectSortOrder === 'z-a' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setProjectSortOrder('date-added')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.projectSort.dateAdded')}</span>
{projectSortOrder === 'date-added' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setProjectSortOrder('recent')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.projectSort.recent')}</span>
{projectSortOrder === 'recent' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
@@ -237,6 +293,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
<Icon name="expand-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.expandAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -17,6 +17,7 @@ import { SortableGroupItem, SortableProjectItem } from './sortableItems';
import { formatProjectLabel } from './utils';
import { useI18n } from '@/lib/i18n';
import type { MainTab } from '@/stores/useUIStore';
import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore';
type ProjectSection = {
project: {
@@ -69,6 +70,7 @@ type Props = {
removeProject: (id: string) => void;
projectHeaderSentinelRefs: React.MutableRefObject<Map<string, HTMLDivElement | null>>;
reorderProjects: (fromIndex: number, toIndex: number) => void;
projectSortOrder: ProjectSortOrder;
getOrderedGroups: (projectId: string, groups: SessionGroup[]) => SessionGroup[];
setGroupOrderByProject: React.Dispatch<React.SetStateAction<Map<string, string[]>>>;
openSidebarMenuKey: string | null;
@@ -188,6 +190,8 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
collisionDetection={closestCenter}
onDragEnd={(event) => {
if (props.isInlineEditing) return;
// Drag only allowed in manual sort mode - indices from visual order don't match store order in other modes
if (props.projectSortOrder !== 'manual') return;
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = props.sectionsForRender.findIndex((section) => section.project.id === active.id);
@@ -219,6 +223,7 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
<SortableProjectItem
key={projectKey}
id={projectKey}
disabled={props.projectSortOrder !== 'manual'}
projectLabel={projectLabel}
projectDescription={projectDescription}
projectIcon={project.icon}
@@ -17,6 +17,7 @@ import { useI18n } from '@/lib/i18n';
export interface SortableProjectItemProps {
id: string;
disabled?: boolean;
projectLabel: string;
projectDescription: string;
projectIcon?: string;
@@ -51,6 +52,7 @@ export type SortableDragHandleProps = {
export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
id,
disabled = false,
projectLabel,
projectDescription,
projectIcon,
@@ -85,7 +87,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
transform,
transition,
isDragging,
} = useSortable({ id });
} = useSortable({ id, disabled });
const suppressNextToggleRef = React.useRef(false);
const menuInstanceKey = `project:${id}`;