feat(app): switch sidebar Group by from the command center (#3063)
Switching the sidebar between Project and Status grouping takes three clicks today: open the display preferences menu, open Grouping, pick the mode. It is the only sidebar preference with no fast path. Typing "group" in the command center now offers a single entry that switches to the mode you are not in, so it reads "Group by status" while grouped by project and the reverse once you are in status mode. With two modes a "cycle grouping" label would not say where you land, and an entry that reads the same in both states looks like it does nothing. The entry is query-only, so the default Cmd-K list is unchanged. It does not clear the focus restore element, because it navigates nowhere and focus has to return to whatever you were typing in. The contribution is built by a pure function in root-contributions.ts so its branches can be unit tested, mirroring workspace-contributions.ts. The registration subscribes to groupMode with a narrow selector to avoid re-registering every root action when host filters are reconciled. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b7ca1f7502
commit
07044136c8
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SidebarGroupMode } from "@/stores/sidebar-view-store";
|
||||
import type { CommandCenterIconProps } from "./contributions";
|
||||
import { buildGroupingContribution, type GroupingCommandCenterSource } from "./root-contributions";
|
||||
|
||||
function ProjectIcon(_props: CommandCenterIconProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function StatusIcon(_props: CommandCenterIconProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function source(groupMode: SidebarGroupMode): {
|
||||
value: GroupingCommandCenterSource;
|
||||
applied: SidebarGroupMode[];
|
||||
} {
|
||||
const applied: SidebarGroupMode[] = [];
|
||||
return {
|
||||
value: {
|
||||
groupMode,
|
||||
labels: {
|
||||
section: "Actions",
|
||||
groupByProject: "Group by project",
|
||||
groupByStatus: "Group by status",
|
||||
},
|
||||
icons: { project: ProjectIcon, status: StatusIcon },
|
||||
setGroupMode: (mode) => applied.push(mode),
|
||||
},
|
||||
applied,
|
||||
};
|
||||
}
|
||||
|
||||
describe("grouping command center contribution", () => {
|
||||
it("offers status while grouped by project", () => {
|
||||
const fixture = source("project");
|
||||
const contribution = buildGroupingContribution(fixture.value);
|
||||
|
||||
expect(contribution.presentation).toMatchObject({
|
||||
title: "Group by status",
|
||||
icon: StatusIcon,
|
||||
});
|
||||
|
||||
contribution.run();
|
||||
expect(fixture.applied).toEqual(["status"]);
|
||||
});
|
||||
|
||||
it("offers project while grouped by status", () => {
|
||||
const fixture = source("status");
|
||||
const contribution = buildGroupingContribution(fixture.value);
|
||||
|
||||
expect(contribution.presentation).toMatchObject({
|
||||
title: "Group by project",
|
||||
icon: ProjectIcon,
|
||||
});
|
||||
|
||||
contribution.run();
|
||||
expect(fixture.applied).toEqual(["project"]);
|
||||
});
|
||||
|
||||
it("keeps a stable id across both modes so the registry tiebreak never moves", () => {
|
||||
expect(buildGroupingContribution(source("project").value).id).toBe(
|
||||
buildGroupingContribution(source("status").value).id,
|
||||
);
|
||||
});
|
||||
|
||||
it("stays out of the default empty-query list", () => {
|
||||
for (const mode of ["project", "status"] as const) {
|
||||
const contribution = buildGroupingContribution(source(mode).value);
|
||||
expect(contribution.visibility).toBe("query");
|
||||
expect(contribution.group).toBe("actions");
|
||||
// 6 is keyboard-shortcuts and 7 belongs to the workspace actions in #3013.
|
||||
expect(contribution.rank).toBe(8);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { SidebarGroupMode } from "@/stores/sidebar-view-store";
|
||||
import type { CommandCenterContribution, CommandCenterIcon } from "./contributions";
|
||||
|
||||
export interface GroupingCommandCenterSource {
|
||||
groupMode: SidebarGroupMode;
|
||||
labels: {
|
||||
section: string;
|
||||
groupByProject: string;
|
||||
groupByStatus: string;
|
||||
};
|
||||
icons: {
|
||||
project?: CommandCenterIcon;
|
||||
status?: CommandCenterIcon;
|
||||
};
|
||||
setGroupMode(mode: SidebarGroupMode): void;
|
||||
}
|
||||
|
||||
// One entry that always names the mode you are not in, so it can never read as a no-op.
|
||||
export function buildGroupingContribution(
|
||||
source: GroupingCommandCenterSource,
|
||||
): CommandCenterContribution {
|
||||
// Collapse into nextGroupMode() from sidebar-view-store once #2504 lands.
|
||||
const target: SidebarGroupMode = source.groupMode === "project" ? "status" : "project";
|
||||
return {
|
||||
id: "sidebar-grouping",
|
||||
group: "actions",
|
||||
groupRank: 0,
|
||||
rank: 8,
|
||||
keywords: ["group", "grouping", "sort", "sidebar", "project", "status"],
|
||||
visibility: "query",
|
||||
run: () => source.setGroupMode(target),
|
||||
presentation: {
|
||||
kind: "action",
|
||||
title: target === "status" ? source.labels.groupByStatus : source.labels.groupByProject,
|
||||
sectionTitle: source.labels.section,
|
||||
icon: target === "status" ? source.icons.status : source.icons.project,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { router, type Href } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
CalendarClock,
|
||||
CircleDashed,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
History,
|
||||
Home,
|
||||
@@ -18,6 +20,7 @@ import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"
|
||||
import { useKeyboardShortcutsAvailable } from "@/keyboard/availability";
|
||||
import { resolveShortcutKeysForAction } from "@/keyboard/keyboard-shortcuts";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { useSidebarViewStore } from "@/stores/sidebar-view-store";
|
||||
import { clearCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
||||
import {
|
||||
buildOpenProjectRoute,
|
||||
@@ -28,6 +31,7 @@ import {
|
||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||
import type { CommandCenterContribution, CommandCenterIconProps } from "./contributions";
|
||||
import { useCommandCenterActions } from "./provider";
|
||||
import { buildGroupingContribution } from "./root-contributions";
|
||||
|
||||
const ThemedPlus = withUnistyles(Plus, (theme) => ({ color: theme.colors.foregroundMuted }));
|
||||
const ThemedFolderPlus = withUnistyles(FolderPlus, (theme) => ({
|
||||
@@ -46,6 +50,10 @@ const ThemedSettings = withUnistyles(Settings, (theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
}));
|
||||
const ThemedHome = withUnistyles(Home, (theme) => ({ color: theme.colors.foregroundMuted }));
|
||||
const ThemedFolder = withUnistyles(Folder, (theme) => ({ color: theme.colors.foregroundMuted }));
|
||||
const ThemedCircleDashed = withUnistyles(CircleDashed, (theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
}));
|
||||
|
||||
function PlusIcon({ size }: CommandCenterIconProps) {
|
||||
return <ThemedPlus size={size} strokeWidth={2.4} />;
|
||||
@@ -75,6 +83,14 @@ function HomeIcon({ size }: CommandCenterIconProps) {
|
||||
return <ThemedHome size={size} strokeWidth={2.2} />;
|
||||
}
|
||||
|
||||
function FolderIcon({ size }: CommandCenterIconProps) {
|
||||
return <ThemedFolder size={size} strokeWidth={2.2} />;
|
||||
}
|
||||
|
||||
function CircleDashedIcon({ size }: CommandCenterIconProps) {
|
||||
return <ThemedCircleDashed size={size} strokeWidth={2.2} />;
|
||||
}
|
||||
|
||||
export function CommandCenterRootActions() {
|
||||
const { t } = useTranslation();
|
||||
const { overrides } = useKeyboardShortcutOverrides();
|
||||
@@ -85,6 +101,10 @@ export function CommandCenterRootActions() {
|
||||
const sessionsRoute = useMemo<Href>(() => buildSessionsRoute(), []);
|
||||
const schedulesRoute = useMemo<Href>(() => buildSchedulesRoute(), []);
|
||||
const setShortcutsDialogOpen = useKeyboardShortcutsStore((state) => state.setShortcutsDialogOpen);
|
||||
// Narrow selector on purpose: a whole-store subscription would re-register every root action
|
||||
// each time host filters are reconciled.
|
||||
const groupMode = useSidebarViewStore((state) => state.groupMode);
|
||||
const setGroupMode = useSidebarViewStore((state) => state.setGroupMode);
|
||||
const shortcutPlatform = useMemo(
|
||||
() => ({ isMac: getShortcutOs() === "mac", isDesktop: getIsElectronRuntime() }),
|
||||
[],
|
||||
@@ -229,13 +249,28 @@ export function CommandCenterRootActions() {
|
||||
});
|
||||
}
|
||||
|
||||
availableActions.push(
|
||||
buildGroupingContribution({
|
||||
groupMode,
|
||||
labels: {
|
||||
section: t("shell.commandCenter.actions"),
|
||||
groupByProject: t("shell.commandCenter.groupByProject"),
|
||||
groupByStatus: t("shell.commandCenter.groupByStatus"),
|
||||
},
|
||||
icons: { project: FolderIcon, status: CircleDashedIcon },
|
||||
setGroupMode,
|
||||
}),
|
||||
);
|
||||
|
||||
return availableActions;
|
||||
}, [
|
||||
groupMode,
|
||||
homeRoute,
|
||||
openAddProject,
|
||||
overrides,
|
||||
schedulesRoute,
|
||||
sessionsRoute,
|
||||
setGroupMode,
|
||||
setShortcutsDialogOpen,
|
||||
settingsRoute,
|
||||
shortcutPlatform,
|
||||
|
||||
Reference in New Issue
Block a user