From 410397857895d7e4e63bf8f36e27f578b9dcac5c Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 6 May 2026 08:59:39 -0500 Subject: [PATCH] Polish operator sidebar and issue property controls (#5355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Operators use the board sidebar and issue properties panel to move between companies and understand task metadata > - Small UI regressions in these controls make repeated board operation slower and less predictable > - The local branch already contained targeted fixes for company ordering, issue date display, and sidebar rail sizing > - This pull request isolates those operator UI quality-of-life fixes into a standalone branch against `origin/master` > - The benefit is a focused, reviewable PR that can merge independently of the issue-thread activity work ## What Changed - Shows issue property timestamps with time, not just dates. - Adds edit-mode support for ordering companies in the sidebar company menu. - Fixes a workspace switcher rail regression and keeps the account menu aligned with the rail width. - Includes focused component coverage for the touched controls. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx ui/src/components/SidebarCompanyMenu.test.tsx ui/src/components/Layout.test.tsx ui/src/components/SidebarAccountMenu.test.tsx` — 4 files passed, 29 tests passed. - `pnpm --filter /ui typecheck` - PR checks on `a4030f7a` are green: policy, verify, serialized server suites 1/4-4/4, e2e, Canary Dry Run, Greptile Review, and Snyk. - Captured a local Storybook screenshot of `Product/Navigation & Layout` after the sidebar polish: `/tmp/pap-3659-screenshots/navigation-layout-after.png`. - Confirmed the PR changes 8 files and does not include `pnpm-lock.yaml` or `.github/workflows/*`. ## Risks - Low to moderate UI risk: this touches shared sidebar components and issue metadata rendering. - The company ordering behavior depends on existing query/cache behavior, so stale cache bugs would show up as ordering inconsistencies. - No database, API, workflow, or lockfile changes are included. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, shell/tool-use enabled, used to split the existing branch, verify the isolated PR branch, and create this PR. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- ui/src/components/CompanyRail.tsx | 260 ------------------ ui/src/components/IssueProperties.test.tsx | 19 ++ ui/src/components/IssueProperties.tsx | 8 +- ui/src/components/Layout.test.tsx | 7 +- ui/src/components/Layout.tsx | 3 - ui/src/components/SidebarAccountMenu.test.tsx | 2 + ui/src/components/SidebarAccountMenu.tsx | 2 +- ui/src/components/SidebarCompanyMenu.test.tsx | 73 +++++ ui/src/components/SidebarCompanyMenu.tsx | 228 ++++++++++++--- .../stories/navigation-layout.stories.tsx | 8 - 10 files changed, 297 insertions(+), 313 deletions(-) delete mode 100644 ui/src/components/CompanyRail.tsx diff --git a/ui/src/components/CompanyRail.tsx b/ui/src/components/CompanyRail.tsx deleted file mode 100644 index 2766a16b..00000000 --- a/ui/src/components/CompanyRail.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { Paperclip, Plus } from "lucide-react"; -import { useQueries, useQuery } from "@tanstack/react-query"; -import { - DndContext, - closestCenter, - MouseSensor, - useSensor, - useSensors, - type DragEndEvent, -} from "@dnd-kit/core"; -import { - SortableContext, - useSortable, - verticalListSortingStrategy, - arrayMove, -} from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { useCompany } from "../context/CompanyContext"; -import { useDialogActions } from "../context/DialogContext"; -import { cn } from "../lib/utils"; -import { queryKeys } from "../lib/queryKeys"; -import { sidebarBadgesApi } from "../api/sidebarBadges"; -import { heartbeatsApi } from "../api/heartbeats"; -import { authApi } from "../api/auth"; -import { useCompanyOrder } from "../hooks/useCompanyOrder"; -import { useLocation, useNavigate } from "@/lib/router"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import type { Company } from "@paperclipai/shared"; -import { CompanyPatternIcon } from "./CompanyPatternIcon"; - -function SortableCompanyItem({ - company, - isSelected, - hasLiveAgents, - hasUnreadInbox, - onSelect, -}: { - company: Company; - isSelected: boolean; - hasLiveAgents: boolean; - hasUnreadInbox: boolean; - onSelect: () => void; -}) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: company.id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - zIndex: isDragging ? 10 : undefined, - opacity: isDragging ? 0.8 : 1, - }; - - return ( -
- - - { - if (isDragging) { - e.preventDefault(); - return; - } - e.preventDefault(); - onSelect(); - }} - className="relative flex items-center justify-center group overflow-visible" - > - {/* Selection indicator pill */} -
-
- - {hasLiveAgents && ( - - - - - - - )} - {hasUnreadInbox && ( - - )} -
-
- - -

{company.name}

-
- -
- ); -} - -export function CompanyRail() { - const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany(); - const { openOnboarding } = useDialogActions(); - const navigate = useNavigate(); - const location = useLocation(); - const isInstanceRoute = location.pathname.startsWith("/instance/"); - const highlightedCompanyId = isInstanceRoute ? null : selectedCompanyId; - const sidebarCompanies = useMemo( - () => companies.filter((company) => company.status !== "archived"), - [companies], - ); - const { data: session } = useQuery({ - queryKey: queryKeys.auth.session, - queryFn: () => authApi.getSession(), - }); - const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; - const companyIds = useMemo(() => sidebarCompanies.map((company) => company.id), [sidebarCompanies]); - - const liveRunsQueries = useQueries({ - queries: companyIds.map((companyId) => ({ - queryKey: queryKeys.liveRuns(companyId), - queryFn: () => heartbeatsApi.liveRunsForCompany(companyId), - refetchInterval: 10_000, - })), - }); - const sidebarBadgeQueries = useQueries({ - queries: companyIds.map((companyId) => ({ - queryKey: queryKeys.sidebarBadges(companyId), - queryFn: () => sidebarBadgesApi.get(companyId), - refetchInterval: 15_000, - })), - }); - const hasLiveAgentsByCompanyId = useMemo(() => { - const result = new Map(); - companyIds.forEach((companyId, index) => { - result.set(companyId, (liveRunsQueries[index]?.data?.length ?? 0) > 0); - }); - return result; - }, [companyIds, liveRunsQueries]); - const hasUnreadInboxByCompanyId = useMemo(() => { - const result = new Map(); - companyIds.forEach((companyId, index) => { - result.set(companyId, (sidebarBadgeQueries[index]?.data?.inbox ?? 0) > 0); - }); - return result; - }, [companyIds, sidebarBadgeQueries]); - - const { orderedCompanies, persistOrder } = useCompanyOrder({ - companies: sidebarCompanies, - userId: currentUserId, - }); - - // Require 8px of movement before starting a drag to avoid interfering with clicks - const sensors = useSensors( - // Keep sidebar reordering mouse-only so touch input can scroll/tap without drag affordances. - useSensor(MouseSensor, { - activationConstraint: { distance: 8 }, - }) - ); - - const handleDragEnd = useCallback( - (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const ids = orderedCompanies.map((c) => c.id); - const oldIndex = ids.indexOf(active.id as string); - const newIndex = ids.indexOf(over.id as string); - if (oldIndex === -1 || newIndex === -1) return; - - persistOrder(arrayMove(ids, oldIndex, newIndex)); - }, - [orderedCompanies, persistOrder] - ); - - return ( -
- {/* Paperclip icon - aligned with top sections (implied line, no visible border) */} -
- -
- - {/* Company list */} -
- - c.id)} - strategy={verticalListSortingStrategy} - > - {orderedCompanies.map((company) => ( - { - setSelectedCompanyId(company.id); - if (isInstanceRoute) { - navigate(`/${company.issuePrefix}/dashboard`); - } - }} - /> - ))} - - -
- - {/* Separator before add button */} -
- - {/* Add company button */} -
- - - - - -

Add company

-
-
-
-
- ); -} diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 283f93ce..41f103b2 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -559,6 +559,25 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + it("shows full date and time for issue metadata timestamps", async () => { + const root = renderProperties(container, { + issue: createIssue({ + createdAt: new Date(2026, 3, 6, 12, 34), + startedAt: new Date(2026, 3, 6, 12, 35), + completedAt: new Date(2026, 3, 6, 12, 36), + }), + childIssues: [], + onUpdate: vi.fn(), + }); + await flush(); + + expect(container.textContent).toMatch(/CreatedApr 6, 2026, \d{1,2}:34 (AM|PM)/); + expect(container.textContent).toMatch(/StartedApr 6, 2026, \d{1,2}:35 (AM|PM)/); + expect(container.textContent).toMatch(/CompletedApr 6, 2026, \d{1,2}:36 (AM|PM)/); + + act(() => root.unmount()); + }); + it("shows a workspace tasks link for non-default workspaces when isolated workspaces are enabled", async () => { mockProjectsApi.list.mockResolvedValue([createProject()]); mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true }); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 9039ce7c..968bbc2b 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -29,7 +29,7 @@ import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; import { Identity } from "./Identity"; import { IssueReferencePill } from "./IssueReferencePill"; -import { formatDate, cn, projectUrl } from "../lib/utils"; +import { formatDate, formatDateTime, cn, projectUrl } from "../lib/utils"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { @@ -1592,16 +1592,16 @@ export function IssueProperties({ )} {issue.startedAt && ( - {formatDate(issue.startedAt)} + {formatDateTime(issue.startedAt)} )} {issue.completedAt && ( - {formatDate(issue.completedAt)} + {formatDateTime(issue.completedAt)} )} - {formatDate(issue.createdAt)} + {formatDateTime(issue.createdAt)} {timeAgo(issue.updatedAt)} diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx index 63901e5f..379d6934 100644 --- a/ui/src/components/Layout.test.tsx +++ b/ui/src/components/Layout.test.tsx @@ -40,10 +40,6 @@ vi.mock("@/lib/router", () => ({ }, })); -vi.mock("./CompanyRail", () => ({ - CompanyRail: () =>
Company rail
, -})); - vi.mock("./Sidebar", () => ({ Sidebar: () =>
Main company nav
, })); @@ -260,6 +256,7 @@ describe("Layout", () => { expect(mockHealthApi.get).toHaveBeenCalled(); expect(container.textContent).toContain("Breadcrumbs"); expect(container.textContent).toContain("Outlet content"); + expect(container.textContent).not.toContain("Company rail"); expect(container.textContent).not.toContain("Authenticated private"); expect(container.textContent).not.toContain( "Sign-in is required and this instance is intended for private-network access.", @@ -312,6 +309,7 @@ describe("Layout", () => { await flushReact(); expect(container.textContent).toContain("Company settings sidebar"); + expect(container.textContent).not.toContain("Company rail"); expect(container.textContent).not.toContain("Instance sidebar"); expect(container.textContent).not.toContain("Main company nav"); expect(container.textContent).not.toContain("Plugin route sidebar"); @@ -339,6 +337,7 @@ describe("Layout", () => { await flushReact(); expect(container.textContent).toContain("Instance sidebar"); + expect(container.textContent).not.toContain("Company rail"); expect(container.textContent).not.toContain("Company settings sidebar"); expect(container.textContent).not.toContain("Main company nav"); expect(container.textContent).not.toContain("Plugin route sidebar"); diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index e256a990..5f0567a6 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Outlet, useLocation, useNavigate, useNavigationType, useParams } from "@/lib/router"; -import { CompanyRail } from "./CompanyRail"; import { Sidebar } from "./Sidebar"; import { InstanceSidebar } from "./InstanceSidebar"; import { CompanySettingsSidebar } from "./CompanySettingsSidebar"; @@ -378,7 +377,6 @@ export function Layout() { )} >
-
{isInstanceSettingsRoute ? ( @@ -398,7 +396,6 @@ export function Layout() { ) : (
- {isInstanceSettingsRoute ? ( diff --git a/ui/src/components/SidebarAccountMenu.test.tsx b/ui/src/components/SidebarAccountMenu.test.tsx index 6c1f37c7..72247dec 100644 --- a/ui/src/components/SidebarAccountMenu.test.tsx +++ b/ui/src/components/SidebarAccountMenu.test.tsx @@ -109,6 +109,8 @@ describe("SidebarAccountMenu", () => { expect(document.body.textContent).toContain("Documentation"); expect(document.body.textContent).toContain("Paperclip v1.2.3"); expect(document.body.textContent).toContain("jane@example.com"); + expect(document.body.querySelector('[data-slot="popover-content"]')?.className) + .toContain("w-[var(--radix-popover-trigger-width)]"); await act(async () => { root.unmount(); diff --git a/ui/src/components/SidebarAccountMenu.tsx b/ui/src/components/SidebarAccountMenu.tsx index df97aab4..aa4c4ee4 100644 --- a/ui/src/components/SidebarAccountMenu.tsx +++ b/ui/src/components/SidebarAccountMenu.tsx @@ -160,7 +160,7 @@ export function SidebarAccountMenu({ side="top" align="start" sideOffset={10} - className="w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-t-2xl rounded-b-none border-border p-0 shadow-2xl" + className="w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-1rem)] overflow-hidden rounded-t-2xl rounded-b-none border-border p-0 shadow-2xl" >
diff --git a/ui/src/components/SidebarCompanyMenu.test.tsx b/ui/src/components/SidebarCompanyMenu.test.tsx index 12634662..d1471f2e 100644 --- a/ui/src/components/SidebarCompanyMenu.test.tsx +++ b/ui/src/components/SidebarCompanyMenu.test.tsx @@ -19,11 +19,19 @@ const mockOpenOnboarding = vi.hoisted(() => vi.fn()); const mockSetSelectedCompanyId = vi.hoisted(() => vi.fn()); const mockSetSidebarOpen = vi.hoisted(() => vi.fn()); const mockLocation = vi.hoisted(() => ({ pathname: "/PAP/dashboard" })); +const mockSidebarPreferencesApi = vi.hoisted(() => ({ + getCompanyOrder: vi.fn(), + updateCompanyOrder: vi.fn(), +})); vi.mock("@/api/auth", () => ({ authApi: mockAuthApi, })); +vi.mock("@/api/sidebarPreferences", () => ({ + sidebarPreferencesApi: mockSidebarPreferencesApi, +})); + vi.mock("@/lib/router", () => ({ Link: ({ children, to, ...props }: { children: React.ReactNode; to: string }) => ( {children} @@ -112,6 +120,14 @@ describe("SidebarCompanyMenu", () => { }, }); mockAuthApi.signOut.mockResolvedValue(undefined); + mockSidebarPreferencesApi.getCompanyOrder.mockResolvedValue({ + orderedIds: ["company-1", "company-2", "company-3"], + updatedAt: null, + }); + mockSidebarPreferencesApi.updateCompanyOrder.mockResolvedValue({ + orderedIds: ["company-1", "company-2", "company-3"], + updatedAt: null, + }); mockLocation.pathname = "/PAP/dashboard"; }); @@ -149,6 +165,7 @@ describe("SidebarCompanyMenu", () => { await flushReact(); expect(document.body.textContent).toContain("Switch workspace"); + expect(document.body.textContent).toContain("Edit"); expect(document.body.textContent).toContain("Strata"); expect(document.body.textContent).toContain("ANA"); expect(document.body.textContent).toContain("Add company..."); @@ -172,6 +189,62 @@ describe("SidebarCompanyMenu", () => { }); }); + it("toggles company order editing without selecting a workspace", async () => { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + await flushReact(); + + const trigger = container.querySelector('button[aria-label="Open Acme Labs workspace switcher"]'); + expect(trigger).not.toBeNull(); + + await act(async () => { + trigger?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0 })); + trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const editButton = Array.from(document.body.querySelectorAll("button")) + .find((element) => element.textContent === "Edit"); + expect(editButton).toBeTruthy(); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(document.body.textContent).toContain("Done"); + expect(document.body.textContent).not.toContain("PAP"); + expect(document.body.textContent).not.toContain("ANA"); + expect(document.body.querySelector('button[aria-label="Reorder Strata"]')).toBeTruthy(); + + const strataItem = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-item"]')) + .find((element) => element.textContent?.includes("Strata")); + expect(strataItem).toBeTruthy(); + + await act(async () => { + strataItem?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(mockSetSelectedCompanyId).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); + + await act(async () => { + root.unmount(); + }); + }); + it("navigates to the selected workspace dashboard from company-prefixed routes", async () => { mockLocation.pathname = "/PAP/issues"; const root = createRoot(container); diff --git a/ui/src/components/SidebarCompanyMenu.tsx b/ui/src/components/SidebarCompanyMenu.tsx index 9cc4dc56..ff6f336d 100644 --- a/ui/src/components/SidebarCompanyMenu.tsx +++ b/ui/src/components/SidebarCompanyMenu.tsx @@ -1,6 +1,25 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Check, ChevronsUpDown, LogOut, Plus, Settings, UserPlus } from "lucide-react"; +import { + Check, + ChevronsUpDown, + GripVertical, + LogOut, + Plus, + Settings, + UserPlus, +} from "lucide-react"; +import { + DndContext, + MouseSensor, + TouchSensor, + closestCenter, + type DragEndEvent, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import type { Company } from "@paperclipai/shared"; import { Link, useLocation, useNavigate } from "@/lib/router"; import { authApi } from "@/api/auth"; @@ -15,6 +34,7 @@ import { } from "@/components/ui/dropdown-menu"; import { useCompany } from "@/context/CompanyContext"; import { useDialogActions } from "@/context/DialogContext"; +import { useCompanyOrder } from "@/hooks/useCompanyOrder"; import { queryKeys } from "@/lib/queryKeys"; import { cn } from "@/lib/utils"; import { useSidebar } from "../context/SidebarContext"; @@ -36,8 +56,81 @@ function WorkspaceIcon({ company }: { company: Company }) { ); } +function SortableCompanyItem({ + company, + isEditing, + isSelected, + onSelect, +}: { + company: Company; + isEditing: boolean; + isSelected: boolean; + onSelect: (company: Company) => void; +}) { + const { + attributes, + listeners, + setActivatorNodeRef, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: company.id, disabled: !isEditing }); + + return ( + { + if (isEditing) { + event.preventDefault(); + return; + } + onSelect(company); + }} + className={cn( + "min-w-0 gap-2 py-2", + isEditing && "cursor-grab", + isDragging && "opacity-80", + isSelected && "bg-accent text-accent-foreground", + )} + > + + {company.name} + {isEditing ? ( + + ) : ( + <> + + {company.issuePrefix} + + {isSelected ? : null} + + )} + + ); +} + export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: SidebarCompanyMenuProps = {}) { const [internalOpen, setInternalOpen] = useState(false); + const [isEditingOrder, setIsEditingOrder] = useState(false); const queryClient = useQueryClient(); const { companies, selectedCompany, setSelectedCompanyId } = useCompany(); const { openOnboarding } = useDialogActions(); @@ -46,6 +139,14 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb const navigate = useNavigate(); const open = controlledOpen ?? internalOpen; const setOpen = onOpenChange ?? setInternalOpen; + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { distance: 8 }, + }), + useSensor(TouchSensor, { + activationConstraint: { delay: 180, tolerance: 6 }, + }), + ); const sidebarCompanies = useMemo( () => companies.filter((company) => company.status !== "archived"), [companies], @@ -55,6 +156,11 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb queryFn: () => authApi.getSession(), retry: false, }); + const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; + const { orderedCompanies, persistOrder } = useCompanyOrder({ + companies: sidebarCompanies, + userId: currentUserId, + }); const signOutMutation = useMutation({ mutationFn: () => authApi.signOut(), @@ -65,8 +171,14 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb }, }); + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) setIsEditingOrder(false); + setOpen(nextOpen); + } + function closeNavigationChrome() { setOpen(false); + setIsEditingOrder(false); if (isMobile) setSidebarOpen(false); } @@ -92,8 +204,23 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb openOnboarding(); } + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const ids = orderedCompanies.map((company) => company.id); + const oldIndex = ids.indexOf(active.id as string); + const newIndex = ids.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return; + + persistOrder(arrayMove(ids, oldIndex, newIndex)); + }, + [orderedCompanies, persistOrder], + ); + return ( - + +
- {sidebarCompanies.map((company) => { - const isSelected = company.id === selectedCompany?.id; - return ( - selectCompany(company)} - className={cn( - "min-w-0 gap-2 py-2", - isSelected && "bg-accent text-accent-foreground", - )} - > - - {company.name} - - {company.issuePrefix} - - {isSelected ? : null} - - ); - })} - {sidebarCompanies.length === 0 ? ( + + company.id)} + strategy={verticalListSortingStrategy} + > + {orderedCompanies.map((company) => ( + + ))} + + + {orderedCompanies.length === 0 ? ( No workspaces ) : null}
- + Add company... - - + + { + if (isEditingOrder) { + event.preventDefault(); + return; + } + closeNavigationChrome(); + }} + > {selectedCompany ? `Invite people to ${selectedCompany.name}` : "Invite people"} - - + + { + if (isEditingOrder) { + event.preventDefault(); + return; + } + closeNavigationChrome(); + }} + > Company settings @@ -164,7 +326,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb signOutMutation.mutate()} - disabled={signOutMutation.isPending} + disabled={isEditingOrder || signOutMutation.isPending} > {signOutMutation.isPending ? "Signing out..." : "Sign out"} diff --git a/ui/storybook/stories/navigation-layout.stories.tsx b/ui/storybook/stories/navigation-layout.stories.tsx index 89e95105..2e87c862 100644 --- a/ui/storybook/stories/navigation-layout.stories.tsx +++ b/ui/storybook/stories/navigation-layout.stories.tsx @@ -11,7 +11,6 @@ import { } from "lucide-react"; import { BreadcrumbBar } from "@/components/BreadcrumbBar"; import { CommandPalette } from "@/components/CommandPalette"; -import { CompanyRail } from "@/components/CompanyRail"; import { CompanySwitcher } from "@/components/CompanySwitcher"; import { KeyboardShortcutsCheatsheetContent } from "@/components/KeyboardShortcutsCheatsheet"; import { MobileBottomNav } from "@/components/MobileBottomNav"; @@ -75,7 +74,6 @@ function SidebarShell({ collapsed = false }: { collapsed?: boolean }) { return (
-
@@ -249,12 +247,6 @@ function NavigationLayoutStories() {
-
-
- -
-
-