From 87016bc2c2a0dcee74d81bc6130e61a8eac7907d Mon Sep 17 00:00:00 2001 From: "Harun R. Rayhan" Date: Mon, 9 Nov 2026 13:30:31 +0600 Subject: [PATCH 01/13] fix(web): manual project order can be changed again Manual project order had no way to change it after the legacy sidebar's drag was removed. Settings > Projects now lists projects in sidebar order with drag handles while the boring sidebar or Manual order is on. Dragging saves the order through the existing reorderProjects store action and switches Project order to Manual. Fixes #27062 --- .../ProjectOrderList.browser.test.tsx | 81 +++++++++ .../components/settings/ProjectOrderList.tsx | 158 ++++++++++++++++++ .../components/settings/ProjectsSettings.tsx | 7 + .../components/settings/SettingsPanels.tsx | 2 +- docs/user/thread-sidebar.md | 3 +- 5 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/settings/ProjectOrderList.browser.test.tsx create mode 100644 apps/web/src/components/settings/ProjectOrderList.tsx diff --git a/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx new file mode 100644 index 0000000000000..af1d10a50d7e8 --- /dev/null +++ b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; + +import { render } from "../../../test/render"; +import type { SidebarProjectSnapshot } from "../../sidebarProjectGrouping"; +import { useUiStateStore } from "../../uiStateStore"; +import { ProjectOrderList } from "./ProjectOrderList"; + +const state = vi.hoisted(() => ({ + groups: [] as SidebarProjectSnapshot[], + updateClientSettings: vi.fn(() => Promise.resolve()), +})); + +vi.mock(import("../../hooks/useSidebarProjectGroups"), () => ({ + useSidebarProjectGroups: () => state.groups, +})); +vi.mock(import("../../hooks/useSettings"), async (original) => ({ + ...(await original()), + updateClientSettings: state.updateClientSettings, +})); +vi.mock(import("../../state/entities"), async (original) => ({ + ...(await original()), + useProjects: () => [], +})); +vi.mock(import("./SettingsScopeContext"), async (original) => ({ + ...(await original()), + useSettingsScope: () => ({ selectScope: vi.fn() }) as never, +})); +vi.mock(import("../ProjectFavicon"), () => ({ ProjectFavicon: () => })); + +function group(name: string, memberKeys: readonly string[], pinned = false) { + return { + projectKey: name, + displayName: name, + workspaceRoot: `/code/${name}`, + pinned, + archived: false, + memberProjects: memberKeys.map((physicalProjectKey) => ({ physicalProjectKey })), + } as unknown as SidebarProjectSnapshot; +} + +function setup(groups: SidebarProjectSnapshot[]) { + state.groups = groups; + state.updateClientSettings.mockClear(); + useUiStateStore.setState({ projectOrder: [] }); +} + +async function moveDown(name: string) { + const handle = page.getByRole("button", { name: new RegExp(`^Reorder ${name},`) }); + await expect.element(handle).toBeVisible(); + handle.element().focus(); + await userEvent.keyboard(" "); + await expect.element(handle).toHaveAttribute("aria-pressed", "true"); + await userEvent.keyboard("{ArrowDown}"); + await userEvent.keyboard(" "); + await expect.element(handle).not.toHaveAttribute("aria-pressed", "true"); +} + +describe("ProjectOrderList", () => { + it("saves a dragged project's checkouts together and switches to manual order", async () => { + setup([group("alpha", ["a-local", "a-remote"]), group("beta", ["b"]), group("gamma", ["g"])]); + await using _screen = await render(); + + await moveDown("alpha"); + + expect(useUiStateStore.getState().projectOrder).toEqual(["b", "a-local", "a-remote", "g"]); + expect(state.updateClientSettings).toHaveBeenCalledWith({ + sidebarProjectSortOrder: "manual", + }); + }); + + it("leaves the order alone when a drop crosses the pinned line", async () => { + setup([group("pinned", ["p"], true), group("beta", ["b"])]); + await using _screen = await render(); + + await moveDown("pinned"); + + expect(useUiStateStore.getState().projectOrder).toEqual([]); + expect(state.updateClientSettings).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/components/settings/ProjectOrderList.tsx b/apps/web/src/components/settings/ProjectOrderList.tsx new file mode 100644 index 0000000000000..6bb4b86b08494 --- /dev/null +++ b/apps/web/src/components/settings/ProjectOrderList.tsx @@ -0,0 +1,158 @@ +import { + DndContext, + type DragEndEvent, + KeyboardSensor, + PointerSensor, + closestCenter, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + pickableProjectGroups, + selectProjectGroupingSettings, +} from "@t3tools/client-runtime/state/project-grouping"; +import { ChevronRightIcon, GripVerticalIcon } from "lucide-react"; + +import { useClientSettings, updateClientSettings } from "../../hooks/useSettings"; +import { useSidebarProjectGroups } from "../../hooks/useSidebarProjectGroups"; +import { cn } from "../../lib/utils"; +import { + getSidebarProjectSettingsKey, + type SidebarProjectSnapshot, +} from "../../sidebarProjectGrouping"; +import { useProjects } from "../../state/entities"; +import { useUiStateStore } from "../../uiStateStore"; +import { ProjectFavicon } from "../ProjectFavicon"; +import { ITEM_ROW_CLASSNAME } from "./itemRows"; +import { SettingsGroup } from "./SettingsGroup"; +import { SettingsPageContainer } from "./settingsLayout"; +import { useSettingsScope } from "./SettingsScopeContext"; + +const memberKeys = (group: SidebarProjectSnapshot) => + group.memberProjects.map((member) => member.physicalProjectKey); + +export function ProjectOrderList() { + const groups = useSidebarProjectGroups(); + const projects = useProjects(); + const groupingSettings = useClientSettings(selectProjectGroupingSettings); + const reorderProjects = useUiStateStore((store) => store.reorderProjects); + const { selectScope } = useSettingsScope(); + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), + ); + const shown = pickableProjectGroups(groups); + + const handleDragEnd = ({ active, over }: DragEndEvent) => { + const dragged = shown.find((group) => group.projectKey === active.id); + const target = shown.find((group) => group.projectKey === over?.id); + // Pinned projects always sort first, so a drop across that line could not stick. + if (!dragged || !target || dragged === target || dragged.pinned !== target.pinned) return; + reorderProjects(groups.flatMap(memberKeys), memberKeys(dragged), memberKeys(target)); + void updateClientSettings({ sidebarProjectSortOrder: "manual" }); + }; + + return ( + +
+

+ Choose a project to manage its name, icon, checkouts and actions. Drag a project to set + its place in the sidebar. +

+ + + group.projectKey)} + strategy={verticalListSortingStrategy} + > +
    + {shown.map((group, index) => ( + + selectScope({ + project: getSidebarProjectSettingsKey(group, groupingSettings, projects), + }) + } + /> + ))} +
+
+
+
+
+
+ ); +} + +function SortableProjectRow({ + group, + position, + onOpen, +}: { + readonly group: SidebarProjectSnapshot; + readonly position: number; + readonly onOpen: () => void; +}) { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: group.projectKey }); + + return ( +
  • + + +
  • + ); +} diff --git a/apps/web/src/components/settings/ProjectsSettings.tsx b/apps/web/src/components/settings/ProjectsSettings.tsx index 826c92bca26e3..2c9468d4e2e47 100644 --- a/apps/web/src/components/settings/ProjectsSettings.tsx +++ b/apps/web/src/components/settings/ProjectsSettings.tsx @@ -1,7 +1,9 @@ import { EnvironmentId } from "@t3tools/contracts"; import { LayersIcon } from "lucide-react"; +import { useClientSettings } from "../../hooks/useSettings"; import { Button } from "../ui/button"; +import { ProjectOrderList } from "./ProjectOrderList"; import { ProjectSettingsPanel } from "./ProjectSettingsPanel"; import { useSettingsScope } from "./SettingsScopeContext"; import { SettingsScopeNotice } from "./SettingsScopeNotice"; @@ -10,6 +12,9 @@ import { ALL_ENVIRONMENTS_VALUE, selectEnvironmentAxis } from "./settingsScopeAx export function ProjectsSettings() { const { search: value, scope, selectScope } = useSettingsScope(); + const canReorderProjects = useClientSettings( + (settings) => settings.boringSidebarEnabled || settings.sidebarProjectSortOrder === "manual", + ); const unavailable = scope.kind === "unavailable" ? ( @@ -45,6 +50,8 @@ export function ProjectsSettings() { /> ) : unavailable ? ( unavailable + ) : canReorderProjects ? ( + ) : ( Choose a project to manage its name, icon, checkouts and actions. diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 8df97a2ba0aaa..9c1d032ef8eb8 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -809,7 +809,7 @@ export function GeneralSettingsPanel() { /> Date: Mon, 9 Nov 2026 14:26:56 +0600 Subject: [PATCH 02/13] fix(web): project ordering has its own switch and numbered rows A Manual project order switch on Settings > Projects now turns ordering on and off, instead of a drag silently changing the setting. Each row shows its position. --- .../ProjectOrderList.browser.test.tsx | 12 +-- .../components/settings/ProjectOrderList.tsx | 101 +++++++++++------- .../components/settings/ProjectsSettings.tsx | 21 ++-- docs/user/thread-sidebar.md | 4 +- 4 files changed, 80 insertions(+), 58 deletions(-) diff --git a/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx index af1d10a50d7e8..a271a52ee9cb6 100644 --- a/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx @@ -8,16 +8,11 @@ import { ProjectOrderList } from "./ProjectOrderList"; const state = vi.hoisted(() => ({ groups: [] as SidebarProjectSnapshot[], - updateClientSettings: vi.fn(() => Promise.resolve()), })); vi.mock(import("../../hooks/useSidebarProjectGroups"), () => ({ useSidebarProjectGroups: () => state.groups, })); -vi.mock(import("../../hooks/useSettings"), async (original) => ({ - ...(await original()), - updateClientSettings: state.updateClientSettings, -})); vi.mock(import("../../state/entities"), async (original) => ({ ...(await original()), useProjects: () => [], @@ -41,7 +36,6 @@ function group(name: string, memberKeys: readonly string[], pinned = false) { function setup(groups: SidebarProjectSnapshot[]) { state.groups = groups; - state.updateClientSettings.mockClear(); useUiStateStore.setState({ projectOrder: [] }); } @@ -57,16 +51,13 @@ async function moveDown(name: string) { } describe("ProjectOrderList", () => { - it("saves a dragged project's checkouts together and switches to manual order", async () => { + it("saves a dragged project's checkouts together", async () => { setup([group("alpha", ["a-local", "a-remote"]), group("beta", ["b"]), group("gamma", ["g"])]); await using _screen = await render(); await moveDown("alpha"); expect(useUiStateStore.getState().projectOrder).toEqual(["b", "a-local", "a-remote", "g"]); - expect(state.updateClientSettings).toHaveBeenCalledWith({ - sidebarProjectSortOrder: "manual", - }); }); it("leaves the order alone when a drop crosses the pinned line", async () => { @@ -76,6 +67,5 @@ describe("ProjectOrderList", () => { await moveDown("pinned"); expect(useUiStateStore.getState().projectOrder).toEqual([]); - expect(state.updateClientSettings).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/src/components/settings/ProjectOrderList.tsx b/apps/web/src/components/settings/ProjectOrderList.tsx index 6bb4b86b08494..882fbe02d0050 100644 --- a/apps/web/src/components/settings/ProjectOrderList.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.tsx @@ -19,6 +19,7 @@ import { pickableProjectGroups, selectProjectGroupingSettings, } from "@t3tools/client-runtime/state/project-grouping"; +import { DEFAULT_SIDEBAR_PROJECT_SORT_ORDER } from "@t3tools/contracts/settings"; import { ChevronRightIcon, GripVerticalIcon } from "lucide-react"; import { useClientSettings, updateClientSettings } from "../../hooks/useSettings"; @@ -31,14 +32,37 @@ import { import { useProjects } from "../../state/entities"; import { useUiStateStore } from "../../uiStateStore"; import { ProjectFavicon } from "../ProjectFavicon"; +import { Switch } from "../ui/switch"; import { ITEM_ROW_CLASSNAME } from "./itemRows"; import { SettingsGroup } from "./SettingsGroup"; -import { SettingsPageContainer } from "./settingsLayout"; +import { SettingsRow, SettingsSection } from "./settingsLayout"; import { useSettingsScope } from "./SettingsScopeContext"; const memberKeys = (group: SidebarProjectSnapshot) => group.memberProjects.map((member) => member.physicalProjectKey); +export function ManualProjectOrderSection({ enabled }: { readonly enabled: boolean }) { + return ( + + + void updateClientSettings({ + sidebarProjectSortOrder: checked ? "manual" : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, + }) + } + aria-label="Manual project order" + /> + } + /> + + ); +} + export function ProjectOrderList() { const groups = useSidebarProjectGroups(); const projects = useProjects(); @@ -57,46 +81,43 @@ export function ProjectOrderList() { // Pinned projects always sort first, so a drop across that line could not stick. if (!dragged || !target || dragged === target || dragged.pinned !== target.pinned) return; reorderProjects(groups.flatMap(memberKeys), memberKeys(dragged), memberKeys(target)); - void updateClientSettings({ sidebarProjectSortOrder: "manual" }); }; return ( - -
    -

    - Choose a project to manage its name, icon, checkouts and actions. Drag a project to set - its place in the sidebar. -

    - - +

    + Choose a project to manage its name, icon, checkouts and actions. Drag a project to set its + place in the sidebar. +

    + + + group.projectKey)} + strategy={verticalListSortingStrategy} > - group.projectKey)} - strategy={verticalListSortingStrategy} - > -
      - {shown.map((group, index) => ( - - selectScope({ - project: getSidebarProjectSettingsKey(group, groupingSettings, projects), - }) - } - /> - ))} -
    -
    -
    -
    -
    -
    +
      + {shown.map((group, index) => ( + + selectScope({ + project: getSidebarProjectSettingsKey(group, groupingSettings, projects), + }) + } + /> + ))} +
    + + + + ); } @@ -139,6 +160,12 @@ function SortableProjectRow({ > + + {position} + + ); + } + return ( + + ); +} + function SortableProjectRow({ group, position, + canPin, + onTogglePin, onOpen, }: { readonly group: SidebarProjectSnapshot; readonly position: number; + readonly canPin: boolean; + readonly onTogglePin: () => void; readonly onOpen: () => void; }) { const { @@ -123,7 +196,7 @@ function SortableProjectRow({ style={{ transform: CSS.Translate.toString(transform), transition }} className={cn( ITEM_ROW_CLASSNAME, - "flex items-center gap-2", + "group/order flex items-center gap-2", isDragging && "relative z-10 rounded-md bg-background shadow-md", )} > @@ -157,6 +230,12 @@ function SortableProjectRow({
    + ); } diff --git a/apps/web/src/components/settings/ProjectsSettings.tsx b/apps/web/src/components/settings/ProjectsSettings.tsx index 5726cd5edc491..0bc1bee74ee2e 100644 --- a/apps/web/src/components/settings/ProjectsSettings.tsx +++ b/apps/web/src/components/settings/ProjectsSettings.tsx @@ -12,8 +12,8 @@ import { ALL_ENVIRONMENTS_VALUE, selectEnvironmentAxis } from "./settingsScopeAx export function ProjectsSettings() { const { search: value, scope, selectScope } = useSettingsScope(); - const canOrderProjects = useClientSettings( - (settings) => settings.boringSidebarEnabled || settings.sidebarProjectSortOrder === "manual", + const manualOrder = useClientSettings( + (settings) => settings.sidebarProjectSortOrder === "manual", ); const unavailable = scope.kind === "unavailable" ? ( @@ -52,7 +52,7 @@ export function ProjectsSettings() { unavailable ) : ( - {canOrderProjects ? ( + {manualOrder ? ( ) : ( diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 768ce76c2238b..36b1403e295d6 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -473,6 +473,13 @@ export const SETTINGS_SEARCH_ITEMS = [ targetId: "boring-sidebar", searchTerms: ["beta boring sidebar folder rows multiline branch pull request details"], }, + { + id: "boring-sidebar-project-order", + title: "Customize project order", + to: "/settings/labs", + targetId: "boring-sidebar", + searchTerms: ["beta boring sidebar folder order manual drag number sort projects"], + }, { id: "tool-call-visibility", title: "Tool calls in chat", diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 76777b6ae9262..a51b1ae6cf8c6 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -150,9 +150,9 @@ cannot drag or move threads within it. Your saved order returns when you turn it ### Group threads by project (beta) Turn on **Settings → Labs → Boring sidebar (group by project)** on web and desktop to put active -threads in a folder for each project. Pinned projects come first. To fix the folder order, drag projects into place in -**Settings → Projects**. Agent activity won't move them after that; change **Project order** in -**Settings → General** to go back. A project with no active +threads in a folder for each project. Pinned projects come first. To fix the folder order, turn on **Customize project order** +under that setting and drag projects into place in **Settings → Projects**. Agent activity won't +move them while it is on. A project with no active threads has no folder unless it is pinned or you pick it in the project filter. An archived project's folder comes last and shows only while it has active threads. Click a folder to open or close it. A closed folder shows how many threads it holds, and a dot when one of them From ea112dba2265b0ce71d85e5ada81c611934a7e2c Mon Sep 17 00:00:00 2001 From: "Harun R. Rayhan" Date: Mon, 9 Nov 2026 19:14:33 +0600 Subject: [PATCH 05/13] fix(web): project list keeps its arrow last, after pin --- apps/web/src/components/settings/ProjectOrderList.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/ProjectOrderList.tsx b/apps/web/src/components/settings/ProjectOrderList.tsx index 07cd27622d003..1ff0d872348c6 100644 --- a/apps/web/src/components/settings/ProjectOrderList.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.tsx @@ -228,7 +228,6 @@ function SortableProjectRow({ {group.workspaceRoot} - + ); } From 7c6ea4c9bb0858baa8a35fc59b32bb45ba1744ef Mon Sep 17 00:00:00 2001 From: "Harun R. Rayhan" Date: Mon, 9 Nov 2026 19:20:15 +0600 Subject: [PATCH 06/13] fix(web): dragging onto a grouped project keeps its checkouts together Moving a project down onto a project with several checkouts inserted it after the first checkout, splitting them if grouping was later turned off. --- apps/web/src/uiStateStore.test.ts | 11 +++++++++++ apps/web/src/uiStateStore.ts | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts index dbfb60d15bd20..11929e909b224 100644 --- a/apps/web/src/uiStateStore.test.ts +++ b/apps/web/src/uiStateStore.test.ts @@ -91,6 +91,17 @@ describe("uiStateStore pure functions", () => { const next = reorderProjects(makeUiState(), currentOrder, [keyALocal, keyARemote], [keyC]); expect(next.projectOrder).toEqual([keyB, keyC, keyALocal, keyARemote]); + expect( + reorderProjects(makeUiState(), currentOrder, [keyB], [keyALocal, keyARemote]).projectOrder, + ).toEqual([keyB, keyALocal, keyARemote, keyC]); + expect( + reorderProjects( + makeUiState(), + [keyB, keyALocal, keyARemote, keyC], + [keyB], + [keyALocal, keyARemote], + ).projectOrder, + ).toEqual([keyALocal, keyARemote, keyB, keyC]); }); }); diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts index 328e1a6477132..5993da8666af9 100644 --- a/apps/web/src/uiStateStore.ts +++ b/apps/web/src/uiStateStore.ts @@ -420,7 +420,10 @@ export function reorderProjects( return state; } - const insertIndex = originalTargetIndex - Math.max(0, draggedBeforeTarget - 1); + const insertIndex = + draggedBeforeTarget > 0 + ? projectOrder.findLastIndex((id) => targetSet.has(id)) + 1 + : originalTargetIndex; projectOrder.splice(insertIndex, 0, ...removed); return { ...state, From 31deb1d3b0f88305ff2cf515532bc7a45a71101f Mon Sep 17 00:00:00 2001 From: "Harun R. Rayhan" Date: Mon, 9 Nov 2026 19:30:00 +0600 Subject: [PATCH 07/13] fix(web): project reorder announcements read project names --- .../components/settings/ProjectOrderList.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/web/src/components/settings/ProjectOrderList.tsx b/apps/web/src/components/settings/ProjectOrderList.tsx index 1ff0d872348c6..29c61ffa26ed6 100644 --- a/apps/web/src/components/settings/ProjectOrderList.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.tsx @@ -1,8 +1,10 @@ import { + type Announcements, DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, + type UniqueIdentifier, closestCenter, useSensor, useSensors, @@ -53,6 +55,20 @@ export function ProjectOrderList() { ); const shown = pickableProjectGroups(groups); + // dnd-kit announces raw ids by default, and these ids are internal project keys. + const nameOf = (id: UniqueIdentifier) => + shown.find((group) => group.projectKey === id)?.displayName ?? String(id); + const announcements: Announcements = { + onDragStart: ({ active }) => `Picked up ${nameOf(active.id)}.`, + onDragOver: ({ active, over }) => + over ? `${nameOf(active.id)} is over ${nameOf(over.id)}.` : undefined, + onDragEnd: ({ active, over }) => + over + ? `${nameOf(active.id)} was dropped on ${nameOf(over.id)}.` + : `${nameOf(active.id)} was dropped.`, + onDragCancel: ({ active }) => `Moving ${nameOf(active.id)} was cancelled.`, + }; + const handleDragEnd = ({ active, over }: DragEndEvent) => { const dragged = shown.find((group) => group.projectKey === active.id); const target = shown.find((group) => group.projectKey === over?.id); @@ -72,6 +88,7 @@ export function ProjectOrderList() { sensors={sensors} collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis, restrictToParentElement]} + accessibility={{ announcements }} onDragEnd={handleDragEnd} > Date: Mon, 9 Nov 2026 19:30:41 +0600 Subject: [PATCH 08/13] fix(web): keep the project order list to ordering Pinned rows stay marked so the pinned line makes sense, but pinning itself remains in the project's settings and the sidebar. --- .../ProjectOrderList.browser.test.tsx | 9 -- .../components/settings/ProjectOrderList.tsx | 86 ++----------------- 2 files changed, 8 insertions(+), 87 deletions(-) diff --git a/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx index 5ad2d4e124d52..a271a52ee9cb6 100644 --- a/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.browser.test.tsx @@ -10,15 +10,6 @@ const state = vi.hoisted(() => ({ groups: [] as SidebarProjectSnapshot[], })); -vi.mock(import("../../hooks/useProjectGroupUpdate"), async (original) => ({ - ...(await original()), - useProjectPin: () => ({ - canPinProjectGroup: () => true, - setProjectGroupPinned: vi.fn(), - toggleProjectGroupPin: vi.fn(), - }), -})); - vi.mock(import("../../hooks/useSidebarProjectGroups"), () => ({ useSidebarProjectGroups: () => state.groups, })); diff --git a/apps/web/src/components/settings/ProjectOrderList.tsx b/apps/web/src/components/settings/ProjectOrderList.tsx index 29c61ffa26ed6..418b5c275206e 100644 --- a/apps/web/src/components/settings/ProjectOrderList.tsx +++ b/apps/web/src/components/settings/ProjectOrderList.tsx @@ -21,10 +21,9 @@ import { pickableProjectGroups, selectProjectGroupingSettings, } from "@t3tools/client-runtime/state/project-grouping"; -import { ChevronRightIcon, GripVerticalIcon, PinIcon, PinOffIcon } from "lucide-react"; +import { ChevronRightIcon, GripVerticalIcon, PinIcon } from "lucide-react"; import { Fragment } from "react"; -import { useProjectPin } from "../../hooks/useProjectGroupUpdate"; import { useClientSettings } from "../../hooks/useSettings"; import { useSidebarProjectGroups } from "../../hooks/useSidebarProjectGroups"; import { cn } from "../../lib/utils"; @@ -48,7 +47,6 @@ export function ProjectOrderList() { const groupingSettings = useClientSettings(selectProjectGroupingSettings); const reorderProjects = useUiStateStore((store) => store.reorderProjects); const { selectScope } = useSettingsScope(); - const { canPinProjectGroup, toggleProjectGroupPin } = useProjectPin(); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), @@ -104,8 +102,6 @@ export function ProjectOrderList() { toggleProjectGroupPin(group)} onOpen={() => selectScope({ project: getSidebarProjectSettingsKey(group, groupingSettings, projects), @@ -122,79 +118,13 @@ export function ProjectOrderList() { ); } -const PIN_ACTION_CLASSNAME = - "group/pin flex h-6 w-18 shrink-0 items-center justify-center gap-1 rounded text-xs font-medium outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"; - -function ProjectPinAction({ - name, - pinned, - canPin, - onToggle, -}: { - readonly name: string; - readonly pinned: boolean; - readonly canPin: boolean; - readonly onToggle: () => void; -}) { - const pinnedLabel = ( - <> - - Pinned - - ); - if (!canPin) { - return pinned ? ( - {pinnedLabel} - ) : null; - } - if (pinned) { - return ( - - ); - } - return ( - - ); -} - function SortableProjectRow({ group, position, - canPin, - onTogglePin, onOpen, }: { readonly group: SidebarProjectSnapshot; readonly position: number; - readonly canPin: boolean; - readonly onTogglePin: () => void; readonly onOpen: () => void; }) { const { @@ -213,7 +143,7 @@ function SortableProjectRow({ style={{ transform: CSS.Translate.toString(transform), transition }} className={cn( ITEM_ROW_CLASSNAME, - "group/order flex items-center gap-2", + "flex items-center gap-2", isDragging && "relative z-10 rounded-md bg-background shadow-md", )} > @@ -246,12 +176,12 @@ function SortableProjectRow({ - + {group.pinned ? ( + + + Pinned + + ) : null}