From 077898270facf86ec4e1e2eaed7b1d7a5a624099 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 23 May 2026 20:01:23 -0700 Subject: [PATCH 1/3] Show CLI workspaces in sidebar by default --- .../modules/workspace/commands.tsx | 4 +- .../DeleteWorkspaceMount.tsx | 6 + .../RemoveFromSidebarMount.tsx | 17 +- ...useDashboardSidebarWorkspaceItemActions.ts | 4 +- .../sidebarDefaultVisibility.test.ts | 190 ++++++++++++++++++ .../sidebarDefaultVisibility.ts | 77 +++++++ .../useDashboardSidebarData.ts | 147 +++++++++----- .../_authenticated/_dashboard/layout.tsx | 9 +- .../WorkspaceMissingWorktreeState.tsx | 7 + .../v2-workspace/$workspaceId/page.tsx | 1 + .../useDashboardSidebarState.ts | 72 ++++--- .../stores/delete-workspace-intent.ts | 1 + 12 files changed, 446 insertions(+), 89 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts diff --git a/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx b/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx index 8871c05fe3e..e70594651a8 100644 --- a/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx +++ b/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx @@ -68,7 +68,8 @@ export const workspaceProvider: CommandProvider = { }); } - if (!isMain) { + const projectId = workspace.projectId; + if (!isMain && projectId) { commands.push({ id: `workspace.delete:${workspace.id}`, title: `Delete ${workspace.name}`, @@ -79,6 +80,7 @@ export const workspaceProvider: CommandProvider = { run: () => useDeleteWorkspaceIntent.getState().request({ workspaceId: workspace.id, + projectId, workspaceName: workspace.name, }), }); diff --git a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx index 6565fb5f8f7..651046d443c 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx @@ -1,10 +1,12 @@ import { useCallback } from "react"; import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useDeleteWorkspaceIntent } from "renderer/stores/delete-workspace-intent"; export function DeleteWorkspaceMount() { const target = useDeleteWorkspaceIntent((s) => s.target); const close = useDeleteWorkspaceIntent((s) => s.close); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const handleOpenChange = useCallback( (open: boolean) => { @@ -20,6 +22,10 @@ export function DeleteWorkspaceMount() { workspaceName={target.workspaceName} open onOpenChange={handleOpenChange} + onDeleted={() => { + hideWorkspaceInSidebar(target.workspaceId, target.projectId); + close(); + }} /> ); } diff --git a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx index 7d6aa148fe5..9431538c6a2 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx @@ -15,8 +15,7 @@ import { useRemoveFromSidebarIntent } from "renderer/stores/remove-workspace-fro export function RemoveFromSidebarMount() { const target = useRemoveFromSidebarIntent((s) => s.target); const clear = useRemoveFromSidebarIntent((s) => s.clear); - const { hideWorkspaceInSidebar, removeWorkspaceFromSidebar } = - useDashboardSidebarState(); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const { navigateAwayFromWorkspace } = useNavigateAwayFromWorkspace(); const handleOpenChange = useCallback( @@ -29,19 +28,9 @@ export function RemoveFromSidebarMount() { const handleConfirm = useCallback(() => { if (!target) return; navigateAwayFromWorkspace(target.workspaceId); - if (target.isMain) { - hideWorkspaceInSidebar(target.workspaceId, target.projectId); - } else { - removeWorkspaceFromSidebar(target.workspaceId); - } + hideWorkspaceInSidebar(target.workspaceId, target.projectId); clear(); - }, [ - target, - navigateAwayFromWorkspace, - hideWorkspaceInSidebar, - removeWorkspaceFromSidebar, - clear, - ]); + }, [target, navigateAwayFromWorkspace, hideWorkspaceInSidebar, clear]); return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 6a7920423cb..c6da3a3f52d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -42,7 +42,7 @@ export function useDashboardSidebarWorkspaceItemActions({ ); const setManualUnread = useV2NotificationStore((s) => s.setManualUnread); const isUnread = useV2WorkspaceIsUnread(workspaceId); - const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = + const { createSection, hideWorkspaceInSidebar, moveWorkspaceToSection } = useDashboardSidebarState(); const [isRenaming, setIsRenaming] = useState(false); @@ -82,7 +82,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }; const handleDeleted = () => { - removeWorkspaceFromSidebar(workspaceId); + hideWorkspaceInSidebar(workspaceId, projectId); }; const handleRemoveFromSidebar = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts new file mode 100644 index 00000000000..51ccd349eb5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, it } from "bun:test"; +import { + buildSidebarProjects, + type SidebarProjectRow, + shouldIncludeSidebarWorkspace, +} from "./sidebarDefaultVisibility"; + +const userId = "user-1"; + +describe("shouldIncludeSidebarWorkspace", () => { + it("includes workspaces with explicit local sidebar state", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "main", + createdByUserId: null, + hasUserHostAccess: false, + hasLocalSidebarState: true, + }, + userId, + ), + ).toBe(true); + }); + + it("defaults current-user worktree workspaces visible when host is accessible", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: userId, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(true); + }); + + it("does not default main, inaccessible, or other-user workspaces visible", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "main", + createdByUserId: userId, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: userId, + hasUserHostAccess: false, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: "user-2", + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: null, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + null, + ), + ).toBe(false); + }); +}); + +describe("buildSidebarProjects", () => { + const date = new Date("2026-01-01T00:00:00.000Z"); + const explicitProject: SidebarProjectRow = { + id: "project-explicit", + name: "Explicit", + slug: "explicit", + githubRepositoryId: null, + githubOwner: null, + githubRepoName: null, + iconUrl: null, + createdAt: date, + updatedAt: date, + isCollapsed: true, + tabOrder: 1, + }; + const { tabOrder: _explicitTabOrder, ...expectedExplicitProject } = + explicitProject; + + it("adds default project rows without replacing explicit projects", () => { + expect( + buildSidebarProjects( + [explicitProject], + [ + { + projectId: "project-explicit", + projectName: "Ignored", + projectSlug: "ignored", + projectGithubRepositoryId: "ignored-repo", + projectGithubOwner: "ignored-owner", + projectGithubRepoName: "ignored-name", + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + { + projectId: "project-default", + projectName: "Default", + projectSlug: "default", + projectGithubRepositoryId: "repo-1", + projectGithubOwner: "owner", + projectGithubRepoName: "repo", + projectIconUrl: "https://example.com/icon.png", + projectCreatedAt: date, + projectUpdatedAt: date, + }, + ], + ), + ).toEqual([ + expectedExplicitProject, + { + id: "project-default", + name: "Default", + slug: "default", + githubRepositoryId: "repo-1", + githubOwner: "owner", + githubRepoName: "repo", + iconUrl: "https://example.com/icon.png", + createdAt: date, + updatedAt: date, + isCollapsed: false, + }, + ]); + }); + + it("sorts explicit projects by tab order and default projects by name after them", () => { + const laterProject: SidebarProjectRow = { + ...explicitProject, + id: "project-later", + name: "Later", + slug: "later", + tabOrder: 2, + }; + + expect( + buildSidebarProjects( + [laterProject, explicitProject], + [ + { + projectId: "project-z", + projectName: "Zulu", + projectSlug: "zulu", + projectGithubRepositoryId: null, + projectGithubOwner: null, + projectGithubRepoName: null, + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + { + projectId: "project-a", + projectName: "Alpha", + projectSlug: "alpha", + projectGithubRepositoryId: null, + projectGithubOwner: null, + projectGithubRepoName: null, + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + ], + ).map((project) => project.id), + ).toEqual(["project-explicit", "project-later", "project-a", "project-z"]); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts new file mode 100644 index 00000000000..e588f3d1b51 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts @@ -0,0 +1,77 @@ +import type { + DashboardSidebarProject, + DashboardSidebarWorkspaceType, +} from "../../types"; + +export type SidebarProjectRow = Omit & { + tabOrder: number; +}; + +export interface SidebarWorkspaceVisibilitySource { + type: DashboardSidebarWorkspaceType; + createdByUserId: string | null; + hasUserHostAccess: boolean; + hasLocalSidebarState: boolean; +} + +export interface SidebarProjectWorkspaceSource { + projectId: string; + projectName: string; + projectSlug: string; + projectGithubRepositoryId: string | null; + projectGithubOwner: string | null | undefined; + projectGithubRepoName: string | null | undefined; + projectIconUrl: string | null; + projectCreatedAt: Date; + projectUpdatedAt: Date; +} + +export function shouldIncludeSidebarWorkspace( + workspace: SidebarWorkspaceVisibilitySource, + currentUserId: string | null, +): boolean { + if (workspace.hasLocalSidebarState) return true; + if (currentUserId === null) return false; + return ( + workspace.type === "worktree" && + workspace.createdByUserId === currentUserId && + workspace.hasUserHostAccess + ); +} + +export function buildSidebarProjects( + explicitProjects: readonly SidebarProjectRow[], + workspaces: readonly SidebarProjectWorkspaceSource[], +): Array> { + const explicitProjectIds = new Set( + explicitProjects.map((project) => project.id), + ); + const defaultProjects = new Map(); + + for (const workspace of workspaces) { + if (explicitProjectIds.has(workspace.projectId)) continue; + if (defaultProjects.has(workspace.projectId)) continue; + + defaultProjects.set(workspace.projectId, { + id: workspace.projectId, + name: workspace.projectName, + slug: workspace.projectSlug, + githubRepositoryId: workspace.projectGithubRepositoryId, + githubOwner: workspace.projectGithubOwner ?? null, + githubRepoName: workspace.projectGithubRepoName ?? null, + iconUrl: workspace.projectIconUrl, + createdAt: workspace.projectCreatedAt, + updatedAt: workspace.projectUpdatedAt, + isCollapsed: false, + tabOrder: Number.MAX_SAFE_INTEGER, + }); + } + + return [...explicitProjects, ...defaultProjects.values()] + .sort((left, right) => { + const orderDelta = left.tabOrder - right.tabOrder; + if (orderDelta !== 0) return orderDelta; + return left.name.localeCompare(right.name); + }) + .map(({ tabOrder: _tabOrder, ...project }) => project); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index 646b1de8f3e..fa2189aa638 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -1,8 +1,9 @@ -import { eq } from "@tanstack/db"; +import { and, eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useQueries, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { useRelayUrl } from "renderer/hooks/useRelayUrl"; +import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; @@ -20,6 +21,11 @@ import { getDashboardSidebarPullRequestQueryKey, type PullRequestQueryTarget, } from "./derivePullRequestQueryTargets"; +import { + buildSidebarProjects, + type SidebarProjectRow, + shouldIncludeSidebarWorkspace, +} from "./sidebarDefaultVisibility"; const MAIN_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; @@ -126,6 +132,8 @@ function useStableDashboardSidebarProjects( export function useDashboardSidebarData() { const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const currentUserId = session?.user?.id ?? null; const relayUrl = useRelayUrl(); const { toggleProjectCollapsed } = useDashboardSidebarState(); const queryClient = useQueryClient(); @@ -175,20 +183,11 @@ export function useDashboardSidebarData() { createdAt: projects.createdAt, updatedAt: projects.updatedAt, isCollapsed: sidebarProjects.isCollapsed, + tabOrder: sidebarProjects.tabOrder, })), [collections], ); - const sidebarProjects = useMemo( - () => - rawSidebarProjects.map((project) => ({ - ...project, - githubOwner: project.githubOwner ?? null, - githubRepoName: project.githubRepoName ?? null, - })), - [rawSidebarProjects], - ); - const { data: sidebarSections = [] } = useLiveQuery( (q) => q @@ -209,41 +208,81 @@ export function useDashboardSidebarData() { const { data: rawSidebarWorkspaces = [] } = useLiveQuery( (q) => q - .from({ sidebarWorkspaces: collections.v2WorkspaceLocalState }) + .from({ workspaces: collections.v2Workspaces }) + .leftJoin( + { userHosts: collections.v2UsersHosts }, + ({ workspaces, userHosts }) => + and( + eq(userHosts.organizationId, workspaces.organizationId), + eq(userHosts.hostId, workspaces.hostId), + eq(userHosts.userId, currentUserId ?? ""), + ), + ) .innerJoin( - { workspaces: collections.v2Workspaces }, - ({ sidebarWorkspaces, workspaces }) => - eq(sidebarWorkspaces.workspaceId, workspaces.id), + { projects: collections.v2Projects }, + ({ workspaces, projects }) => eq(workspaces.projectId, projects.id), ) - .orderBy( - ({ sidebarWorkspaces }) => sidebarWorkspaces.sidebarState.tabOrder, - "asc", + .leftJoin( + { repos: collections.githubRepositories }, + ({ projects, repos }) => eq(projects.githubRepositoryId, repos.id), ) - .select(({ sidebarWorkspaces, workspaces }) => ({ - id: workspaces.id, - projectId: sidebarWorkspaces.sidebarState.projectId, - hostId: workspaces.hostId, - type: workspaces.type, - name: workspaces.name, - branch: workspaces.branch, - taskId: workspaces.taskId, - createdAt: workspaces.createdAt, - updatedAt: workspaces.updatedAt, - isSynced: workspaces.$synced, - tabOrder: sidebarWorkspaces.sidebarState.tabOrder, - sectionId: sidebarWorkspaces.sidebarState.sectionId, - isHidden: sidebarWorkspaces.sidebarState.isHidden, - })), - [collections], + .leftJoin( + { sidebarWorkspaces: collections.v2WorkspaceLocalState }, + ({ workspaces, sidebarWorkspaces }) => + eq(sidebarWorkspaces.workspaceId, workspaces.id), + ) + .select( + ({ sidebarWorkspaces, workspaces, projects, repos, userHosts }) => ({ + id: workspaces.id, + workspaceProjectId: workspaces.projectId, + sidebarProjectId: sidebarWorkspaces?.sidebarState.projectId ?? null, + projectName: projects.name, + projectSlug: projects.slug, + projectGithubRepositoryId: projects.githubRepositoryId, + projectGithubOwner: repos?.owner ?? null, + projectGithubRepoName: repos?.name ?? null, + projectIconUrl: projects.iconUrl, + projectCreatedAt: projects.createdAt, + projectUpdatedAt: projects.updatedAt, + hostId: workspaces.hostId, + type: workspaces.type, + createdByUserId: workspaces.createdByUserId, + hasUserHostAccess: userHosts?.userId != null, + name: workspaces.name, + branch: workspaces.branch, + taskId: workspaces.taskId, + createdAt: workspaces.createdAt, + updatedAt: workspaces.updatedAt, + isSynced: workspaces.$synced, + hasLocalSidebarState: sidebarWorkspaces?.workspaceId != null, + tabOrder: sidebarWorkspaces?.sidebarState.tabOrder ?? null, + sectionId: sidebarWorkspaces?.sidebarState.sectionId ?? null, + isHidden: sidebarWorkspaces?.sidebarState.isHidden ?? false, + }), + ), + [collections, currentUserId], ); const rawSidebarWorkspacesWithHostStatus = useMemo( () => - rawSidebarWorkspaces.map((workspace) => ({ - ...workspace, - hostIsOnline: hostsByMachineId.get(workspace.hostId)?.isOnline ?? false, - pendingTransaction: workspaceTransactionsById[workspace.id] ?? null, - })), - [hostsByMachineId, rawSidebarWorkspaces, workspaceTransactionsById], + rawSidebarWorkspaces + .filter((workspace) => + shouldIncludeSidebarWorkspace(workspace, currentUserId), + ) + .map((workspace) => ({ + ...workspace, + projectId: workspace.sidebarProjectId ?? workspace.workspaceProjectId, + tabOrder: + workspace.tabOrder ?? new Date(workspace.createdAt).getTime(), + hostIsOnline: + hostsByMachineId.get(workspace.hostId)?.isOnline ?? false, + pendingTransaction: workspaceTransactionsById[workspace.id] ?? null, + })), + [ + currentUserId, + hostsByMachineId, + rawSidebarWorkspaces, + workspaceTransactionsById, + ], ); const sidebarWorkspaces = useMemo( @@ -252,10 +291,31 @@ export function useDashboardSidebarData() { ); const localStateWorkspaceIds = useMemo( - () => new Set(rawSidebarWorkspaces.map((workspace) => workspace.id)), + () => + new Set( + rawSidebarWorkspaces + .filter((workspace) => workspace.hasLocalSidebarState) + .map((workspace) => workspace.id), + ), [rawSidebarWorkspaces], ); + const sidebarProjects = useMemo(() => { + const explicitProjects: SidebarProjectRow[] = rawSidebarProjects.map( + (project) => ({ + ...project, + githubOwner: project.githubOwner ?? null, + githubRepoName: project.githubRepoName ?? null, + }), + ); + return buildSidebarProjects(explicitProjects, sidebarWorkspaces); + }, [rawSidebarProjects, sidebarWorkspaces]); + + const sidebarProjectIds = useMemo( + () => new Set(sidebarProjects.map((project) => project.id)), + [sidebarProjects], + ); + const { data: rawLocalMainWorkspaces = [] } = useLiveQuery( (q) => q @@ -305,9 +365,6 @@ export function useDashboardSidebarData() { ]); const visibleSidebarWorkspaces = useMemo(() => { - const sidebarProjectIds = new Set( - sidebarProjects.map((project) => project.id), - ); const autoLocalMainWorkspaces = localMainWorkspaces.filter( (workspace) => !localStateWorkspaceIds.has(workspace.id) && @@ -320,7 +377,7 @@ export function useDashboardSidebarData() { localMainWorkspaces, localStateWorkspaceIds, machineId, - sidebarProjects, + sidebarProjectIds, sidebarWorkspaces, ]); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index 0634c2439ee..dcd37513599 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -44,6 +44,7 @@ type DeleteTarget = | { version: "v2"; workspaceId: string; + projectId: string; workspaceName: string; open: boolean; }; @@ -53,7 +54,7 @@ function DashboardLayout() { const openNewWorkspaceModal = useOpenNewWorkspaceModal(); const isV2CloudEnabled = useIsV2CloudEnabled(); const collections = useCollections(); - const { removeWorkspaceFromSidebar } = useDashboardSidebarState(); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); useDevSeedV2Sidebar(); // Get current workspace from route to pre-select project in new workspace modal const matchRoute = useMatchRoute(); @@ -139,6 +140,7 @@ function DashboardLayout() { ) { setDeleteTarget({ workspaceId: currentV2WorkspaceId, + projectId: currentV2Workspace.projectId, workspaceName: currentV2Workspace.name || currentV2Workspace.branch, version: "v2", open: true, @@ -224,7 +226,10 @@ function DashboardLayout() { ); }} onDeleted={() => { - removeWorkspaceFromSidebar(deleteTarget.workspaceId); + hideWorkspaceInSidebar( + deleteTarget.workspaceId, + deleteTarget.projectId, + ); setDeleteTarget(null); }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx index 8e8dc3080b9..92adf38d6f0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx @@ -3,9 +3,11 @@ import { Link } from "@tanstack/react-router"; import { ArrowRight, FolderX, RefreshCw, Trash2 } from "lucide-react"; import { useState } from "react"; import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; interface WorkspaceMissingWorktreeStateProps { workspaceId: string; + projectId: string; workspaceName: string; branch: string; worktreePath?: string; @@ -15,6 +17,7 @@ interface WorkspaceMissingWorktreeStateProps { export function WorkspaceMissingWorktreeState({ workspaceId, + projectId, workspaceName, branch, worktreePath, @@ -22,6 +25,7 @@ export function WorkspaceMissingWorktreeState({ isRefreshing = false, }: WorkspaceMissingWorktreeStateProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const displayName = workspaceName || branch; return ( @@ -107,6 +111,9 @@ export function WorkspaceMissingWorktreeState({ workspaceName={displayName} open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} + onDeleted={() => { + hideWorkspaceInSidebar(workspaceId, projectId); + }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 6ff7025d831..84382a8ee0c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -87,6 +87,7 @@ function V2WorkspacePage() { return ( ; +type HiddenWorkspaceCollections = Pick; function compareProjectTopLevelItems( left: ProjectTopLevelItem, @@ -82,6 +83,36 @@ function createEmptyPaneLayout(): WorkspaceState { } satisfies WorkspaceState; } +function writeHiddenWorkspaceSidebarState( + collections: HiddenWorkspaceCollections, + workspaceId: string, + projectId: string, +): void { + const existing = collections.v2WorkspaceLocalState.get(workspaceId); + + if (existing) { + collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { + draft.sidebarState.projectId = projectId; + draft.sidebarState.sectionId = null; + draft.sidebarState.isHidden = true; + draft.paneLayout = createEmptyPaneLayout(); + }); + return; + } + + collections.v2WorkspaceLocalState.insert({ + workspaceId, + createdAt: new Date(), + sidebarState: { + projectId, + tabOrder: 0, + sectionId: null, + isHidden: true, + }, + paneLayout: createEmptyPaneLayout(), + }); +} + /** * Rewrites the flat top-level project lane. Workspace items are explicitly * ungrouped by setting sidebarState.projectId and clearing sidebarState.sectionId. @@ -456,47 +487,38 @@ export function useDashboardSidebarState() { const hideWorkspaceInSidebar = useCallback( (workspaceId: string, projectId: string) => { const workspace = collections.v2WorkspaceLocalState.get(workspaceId); - if (!workspace) { - collections.v2WorkspaceLocalState.insert({ - workspaceId, - createdAt: new Date(), - sidebarState: { - projectId, - tabOrder: 0, - sectionId: null, - isHidden: true, - }, - paneLayout: createEmptyPaneLayout(), - }); - return; + if (workspace) { + cleanupWorkspacePaneRuntimes([workspace]); } - - cleanupWorkspacePaneRuntimes([workspace]); - collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { - draft.sidebarState.projectId = projectId; - draft.sidebarState.sectionId = null; - draft.sidebarState.isHidden = true; - draft.paneLayout = createEmptyPaneLayout(); - }); + writeHiddenWorkspaceSidebarState(collections, workspaceId, projectId); }, [collections], ); const removeProjectFromSidebar = useCallback( (projectId: string) => { + const workspaceIds = new Set([ + ...Array.from(collections.v2WorkspaceLocalState.state.values()) + .filter((item) => item.sidebarState.projectId === projectId) + .map((item) => item.workspaceId), + ...Array.from(collections.v2Workspaces.state.values()) + .filter((item) => item.projectId === projectId) + .map((item) => item.id), + ]); const workspaceRows = Array.from( collections.v2WorkspaceLocalState.state.values(), - ).filter((item) => item.sidebarState.projectId === projectId); - const workspaceIds = workspaceRows.map((item) => item.workspaceId); + ).filter((item) => workspaceIds.has(item.workspaceId)); const sectionIds = Array.from( collections.v2SidebarSections.state.values(), ) .filter((item) => item.projectId === projectId) .map((item) => item.sectionId); - if (workspaceIds.length > 0) { + if (workspaceRows.length > 0) { cleanupWorkspacePaneRuntimes(workspaceRows); - collections.v2WorkspaceLocalState.delete(workspaceIds); + } + for (const workspaceId of workspaceIds) { + writeHiddenWorkspaceSidebarState(collections, workspaceId, projectId); } if (sectionIds.length > 0) { collections.v2SidebarSections.delete(sectionIds); diff --git a/apps/desktop/src/renderer/stores/delete-workspace-intent.ts b/apps/desktop/src/renderer/stores/delete-workspace-intent.ts index cb717a9954b..1294cbce163 100644 --- a/apps/desktop/src/renderer/stores/delete-workspace-intent.ts +++ b/apps/desktop/src/renderer/stores/delete-workspace-intent.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; export interface DeleteWorkspaceTarget { workspaceId: string; + projectId: string; workspaceName: string; } From 1dc27fb2251aaad301e76010b96f609e70342acc Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 23 May 2026 22:45:10 -0700 Subject: [PATCH 2/3] Drop projectId plumbing for hideWorkspaceInSidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hideWorkspaceInSidebar now resolves projectId itself from v2WorkspaceLocalState (preserving explicit reparenting) or v2Workspaces. Removes the projectId arg from DeleteWorkspaceTarget, DeleteTarget, WorkspaceMissingWorktreeStateProps, and the command palette intent payload. Also deletes the now-unused removeWorkspaceFromSidebar — every caller switched to hideWorkspaceInSidebar in the previous commit. --- .../modules/workspace/commands.tsx | 4 +-- .../DeleteWorkspaceMount.tsx | 2 +- .../RemoveFromSidebarMount.tsx | 2 +- ...useDashboardSidebarWorkspaceItemActions.ts | 2 +- .../_authenticated/_dashboard/layout.tsx | 7 +----- .../WorkspaceMissingWorktreeState.tsx | 4 +-- .../v2-workspace/$workspaceId/page.tsx | 1 - .../useDashboardSidebarState.ts | 25 ++++++++----------- .../stores/delete-workspace-intent.ts | 1 - 9 files changed, 16 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx b/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx index e70594651a8..8871c05fe3e 100644 --- a/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx +++ b/apps/desktop/src/renderer/commandPalette/modules/workspace/commands.tsx @@ -68,8 +68,7 @@ export const workspaceProvider: CommandProvider = { }); } - const projectId = workspace.projectId; - if (!isMain && projectId) { + if (!isMain) { commands.push({ id: `workspace.delete:${workspace.id}`, title: `Delete ${workspace.name}`, @@ -80,7 +79,6 @@ export const workspaceProvider: CommandProvider = { run: () => useDeleteWorkspaceIntent.getState().request({ workspaceId: workspace.id, - projectId, workspaceName: workspace.name, }), }); diff --git a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx index 651046d443c..bc80d5c1381 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx @@ -23,7 +23,7 @@ export function DeleteWorkspaceMount() { open onOpenChange={handleOpenChange} onDeleted={() => { - hideWorkspaceInSidebar(target.workspaceId, target.projectId); + hideWorkspaceInSidebar(target.workspaceId); close(); }} /> diff --git a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx index 9431538c6a2..d021e21e332 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx @@ -28,7 +28,7 @@ export function RemoveFromSidebarMount() { const handleConfirm = useCallback(() => { if (!target) return; navigateAwayFromWorkspace(target.workspaceId); - hideWorkspaceInSidebar(target.workspaceId, target.projectId); + hideWorkspaceInSidebar(target.workspaceId); clear(); }, [target, navigateAwayFromWorkspace, hideWorkspaceInSidebar, clear]); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index c6da3a3f52d..3675ad5190c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -82,7 +82,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }; const handleDeleted = () => { - hideWorkspaceInSidebar(workspaceId, projectId); + hideWorkspaceInSidebar(workspaceId); }; const handleRemoveFromSidebar = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index dcd37513599..76a2318c948 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -44,7 +44,6 @@ type DeleteTarget = | { version: "v2"; workspaceId: string; - projectId: string; workspaceName: string; open: boolean; }; @@ -140,7 +139,6 @@ function DashboardLayout() { ) { setDeleteTarget({ workspaceId: currentV2WorkspaceId, - projectId: currentV2Workspace.projectId, workspaceName: currentV2Workspace.name || currentV2Workspace.branch, version: "v2", open: true, @@ -226,10 +224,7 @@ function DashboardLayout() { ); }} onDeleted={() => { - hideWorkspaceInSidebar( - deleteTarget.workspaceId, - deleteTarget.projectId, - ); + hideWorkspaceInSidebar(deleteTarget.workspaceId); setDeleteTarget(null); }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx index 92adf38d6f0..402bc32ab54 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx @@ -7,7 +7,6 @@ import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/u interface WorkspaceMissingWorktreeStateProps { workspaceId: string; - projectId: string; workspaceName: string; branch: string; worktreePath?: string; @@ -17,7 +16,6 @@ interface WorkspaceMissingWorktreeStateProps { export function WorkspaceMissingWorktreeState({ workspaceId, - projectId, workspaceName, branch, worktreePath, @@ -112,7 +110,7 @@ export function WorkspaceMissingWorktreeState({ open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} onDeleted={() => { - hideWorkspaceInSidebar(workspaceId, projectId); + hideWorkspaceInSidebar(workspaceId); }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 84382a8ee0c..6ff7025d831 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -87,7 +87,6 @@ function V2WorkspacePage() { return ( { - const workspace = collections.v2WorkspaceLocalState.get(workspaceId); - if (!workspace) return; - cleanupWorkspacePaneRuntimes([workspace]); - collections.v2WorkspaceLocalState.delete(workspaceId); - }, - [collections], - ); - const hideWorkspaceInSidebar = useCallback( - (workspaceId: string, projectId: string) => { - const workspace = collections.v2WorkspaceLocalState.get(workspaceId); - if (workspace) { - cleanupWorkspacePaneRuntimes([workspace]); + (workspaceId: string) => { + const localState = collections.v2WorkspaceLocalState.get(workspaceId); + if (localState) { + cleanupWorkspacePaneRuntimes([localState]); } + const projectId = + localState?.sidebarState.projectId ?? + collections.v2Workspaces.get(workspaceId)?.projectId; + // If neither row exists, the workspace is already gone — the data live + // query will drop it on its own, so we don't need to write a tombstone. + if (!projectId) return; writeHiddenWorkspaceSidebarState(collections, workspaceId, projectId); }, [collections], @@ -540,7 +536,6 @@ export function useDashboardSidebarState() { moveWorkspaceToSectionAtIndex, removeProjectFromSidebar, reorderProjectChildren, - removeWorkspaceFromSidebar, reorderProjects, reorderWorkspaces, renameSection, diff --git a/apps/desktop/src/renderer/stores/delete-workspace-intent.ts b/apps/desktop/src/renderer/stores/delete-workspace-intent.ts index 1294cbce163..cb717a9954b 100644 --- a/apps/desktop/src/renderer/stores/delete-workspace-intent.ts +++ b/apps/desktop/src/renderer/stores/delete-workspace-intent.ts @@ -2,7 +2,6 @@ import { create } from "zustand"; export interface DeleteWorkspaceTarget { workspaceId: string; - projectId: string; workspaceName: string; } From 50c136c4e32b21c233205f266026c2e115078a16 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 23 May 2026 23:27:41 -0700 Subject: [PATCH 3/3] Fix invalid join condition in sidebar workspaces live query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TanStack DB joins only accept a single eq() — the (org, host, user) and() in the leftJoin against v2UsersHosts threw "Join condition must be an equality expression" at runtime, taking down the sidebar. Replace with a separate v2UsersHosts live query filtered by userId, then check (orgId, hostId) membership via Set in the visibility filter. Same pattern useAccessibleV2Workspaces already uses. --- .../useDashboardSidebarData.ts | 100 +++++++++++------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index fa2189aa638..2fde89d8163 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -1,4 +1,4 @@ -import { and, eq } from "@tanstack/db"; +import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useQueries, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef } from "react"; @@ -209,15 +209,6 @@ export function useDashboardSidebarData() { (q) => q .from({ workspaces: collections.v2Workspaces }) - .leftJoin( - { userHosts: collections.v2UsersHosts }, - ({ workspaces, userHosts }) => - and( - eq(userHosts.organizationId, workspaces.organizationId), - eq(userHosts.hostId, workspaces.hostId), - eq(userHosts.userId, currentUserId ?? ""), - ), - ) .innerJoin( { projects: collections.v2Projects }, ({ workspaces, projects }) => eq(workspaces.projectId, projects.id), @@ -231,42 +222,70 @@ export function useDashboardSidebarData() { ({ workspaces, sidebarWorkspaces }) => eq(sidebarWorkspaces.workspaceId, workspaces.id), ) - .select( - ({ sidebarWorkspaces, workspaces, projects, repos, userHosts }) => ({ - id: workspaces.id, - workspaceProjectId: workspaces.projectId, - sidebarProjectId: sidebarWorkspaces?.sidebarState.projectId ?? null, - projectName: projects.name, - projectSlug: projects.slug, - projectGithubRepositoryId: projects.githubRepositoryId, - projectGithubOwner: repos?.owner ?? null, - projectGithubRepoName: repos?.name ?? null, - projectIconUrl: projects.iconUrl, - projectCreatedAt: projects.createdAt, - projectUpdatedAt: projects.updatedAt, - hostId: workspaces.hostId, - type: workspaces.type, - createdByUserId: workspaces.createdByUserId, - hasUserHostAccess: userHosts?.userId != null, - name: workspaces.name, - branch: workspaces.branch, - taskId: workspaces.taskId, - createdAt: workspaces.createdAt, - updatedAt: workspaces.updatedAt, - isSynced: workspaces.$synced, - hasLocalSidebarState: sidebarWorkspaces?.workspaceId != null, - tabOrder: sidebarWorkspaces?.sidebarState.tabOrder ?? null, - sectionId: sidebarWorkspaces?.sidebarState.sectionId ?? null, - isHidden: sidebarWorkspaces?.sidebarState.isHidden ?? false, - }), - ), + .select(({ sidebarWorkspaces, workspaces, projects, repos }) => ({ + id: workspaces.id, + organizationId: workspaces.organizationId, + workspaceProjectId: workspaces.projectId, + sidebarProjectId: sidebarWorkspaces?.sidebarState.projectId ?? null, + projectName: projects.name, + projectSlug: projects.slug, + projectGithubRepositoryId: projects.githubRepositoryId, + projectGithubOwner: repos?.owner ?? null, + projectGithubRepoName: repos?.name ?? null, + projectIconUrl: projects.iconUrl, + projectCreatedAt: projects.createdAt, + projectUpdatedAt: projects.updatedAt, + hostId: workspaces.hostId, + type: workspaces.type, + createdByUserId: workspaces.createdByUserId, + name: workspaces.name, + branch: workspaces.branch, + taskId: workspaces.taskId, + createdAt: workspaces.createdAt, + updatedAt: workspaces.updatedAt, + isSynced: workspaces.$synced, + hasLocalSidebarState: sidebarWorkspaces?.workspaceId != null, + tabOrder: sidebarWorkspaces?.sidebarState.tabOrder ?? null, + sectionId: sidebarWorkspaces?.sidebarState.sectionId ?? null, + isHidden: sidebarWorkspaces?.sidebarState.isHidden ?? false, + })), + [collections], + ); + + // Separate query — TanStack DB join conditions only accept a single + // equality, so we can't filter `v2UsersHosts` by (org, host, user) inline. + const { data: currentUserHosts = [] } = useLiveQuery( + (q) => + q + .from({ userHosts: collections.v2UsersHosts }) + .where(({ userHosts }) => eq(userHosts.userId, currentUserId ?? "")) + .select(({ userHosts }) => ({ + organizationId: userHosts.organizationId, + hostId: userHosts.hostId, + })), [collections, currentUserId], ); + const userHostAccessKeys = useMemo( + () => + new Set( + currentUserHosts.map((row) => `${row.organizationId}:${row.hostId}`), + ), + [currentUserHosts], + ); + const rawSidebarWorkspacesWithHostStatus = useMemo( () => rawSidebarWorkspaces .filter((workspace) => - shouldIncludeSidebarWorkspace(workspace, currentUserId), + shouldIncludeSidebarWorkspace( + { + ...workspace, + hasUserHostAccess: userHostAccessKeys.has( + `${workspace.organizationId}:${workspace.hostId}`, + ), + }, + currentUserId, + ), ) .map((workspace) => ({ ...workspace, @@ -281,6 +300,7 @@ export function useDashboardSidebarData() { currentUserId, hostsByMachineId, rawSidebarWorkspaces, + userHostAccessKeys, workspaceTransactionsById, ], );