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:
committed by
GitHub
co-authored by
bashrusakh
parent
f6326e1c35
commit
bfaf62e222
@@ -921,6 +921,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
color?: string;
|
||||
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
|
||||
iconBackground?: string;
|
||||
addedAt?: number;
|
||||
lastOpenedAt?: number;
|
||||
sidebarCollapsed?: boolean;
|
||||
}>;
|
||||
}, [projects]);
|
||||
|
||||
@@ -1019,13 +1022,84 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
lastRepoStatusRef.current = Boolean(projectRepoStatus.get(activeProjectId));
|
||||
}
|
||||
|
||||
const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection);
|
||||
const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions);
|
||||
const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder);
|
||||
const manualProjectOrder = useProjectsStore((state) => state.manualProjectOrder);
|
||||
|
||||
const recentProjectIds = React.useMemo(() => {
|
||||
const recentSessions = deriveRecentSessions(sessions);
|
||||
if (recentSessions.length === 0) return new Set<string>();
|
||||
|
||||
const pathToId = new Map<string, string>();
|
||||
for (const project of normalizedProjects) {
|
||||
if (project.normalizedPath) {
|
||||
pathToId.set(project.normalizedPath, project.id);
|
||||
}
|
||||
}
|
||||
|
||||
const ids = new Set<string>();
|
||||
for (const session of recentSessions) {
|
||||
const directory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
|
||||
if (directory) {
|
||||
const projectId = pathToId.get(directory);
|
||||
if (projectId) ids.add(projectId);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}, [sessions, normalizedProjects]);
|
||||
|
||||
const sortedProjects = React.useMemo(() => {
|
||||
const list = [...normalizedProjects];
|
||||
|
||||
switch (projectSortOrder) {
|
||||
case 'a-z':
|
||||
list.sort((a, b) => {
|
||||
const aLabel = (a.label || a.path).toLowerCase();
|
||||
const bLabel = (b.label || b.path).toLowerCase();
|
||||
return aLabel.localeCompare(bLabel);
|
||||
});
|
||||
break;
|
||||
case 'z-a':
|
||||
list.sort((a, b) => {
|
||||
const aLabel = (a.label || a.path).toLowerCase();
|
||||
const bLabel = (b.label || b.path).toLowerCase();
|
||||
return bLabel.localeCompare(aLabel);
|
||||
});
|
||||
break;
|
||||
case 'date-added':
|
||||
list.sort((a, b) => (b.addedAt ?? 0) - (a.addedAt ?? 0));
|
||||
break;
|
||||
case 'recent':
|
||||
list.sort((a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0));
|
||||
break;
|
||||
case 'manual': {
|
||||
const orderMap = new Map(manualProjectOrder.map((id, i) => [id, i]));
|
||||
list.sort((a, b) => {
|
||||
const ai = orderMap.get(a.id) ?? Infinity;
|
||||
const bi = orderMap.get(b.id) ?? Infinity;
|
||||
return ai - bi;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectSortOrder === 'manual') {
|
||||
return list;
|
||||
}
|
||||
|
||||
const recent = list.filter((p) => recentProjectIds.has(p.id));
|
||||
const rest = list.filter((p) => !recentProjectIds.has(p.id));
|
||||
return [...recent, ...rest];
|
||||
}, [normalizedProjects, projectSortOrder, manualProjectOrder, recentProjectIds]);
|
||||
|
||||
const {
|
||||
projectSections,
|
||||
groupSearchDataByGroup,
|
||||
sectionsForRender,
|
||||
searchMatchCount,
|
||||
} = useSessionSidebarSections({
|
||||
normalizedProjects,
|
||||
normalizedProjects: sortedProjects,
|
||||
getSessionsForProject,
|
||||
getArchivedSessionsForProject,
|
||||
availableWorktreesByProject,
|
||||
@@ -1131,9 +1205,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
return meta;
|
||||
}, [projectSections, homeDirectory]);
|
||||
|
||||
const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection);
|
||||
const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions);
|
||||
|
||||
const activeNowSessions = React.useMemo(() => {
|
||||
if (!showRecentSection || isVSCode) {
|
||||
return [];
|
||||
@@ -1636,6 +1707,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
removeProject={removeProject}
|
||||
projectHeaderSentinelRefs={projectHeaderSentinelRefs}
|
||||
reorderProjects={reorderProjects}
|
||||
projectSortOrder={projectSortOrder}
|
||||
getOrderedGroups={getOrderedGroups}
|
||||
setGroupOrderByProject={setGroupOrderByProject}
|
||||
openSidebarMenuKey={openSidebarMenuKey}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user