From b184e40108fa64590326b37a7da919a404bc8693 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:42:47 -0600 Subject: [PATCH] perf(ui): serve every project list from one includeArchived read (#20395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The board UI reads the company project list in many places: the sidebar, starred projects, the command palette, the new-task dialog, and archive-aware pages such as the Mine inbox > - Active-only consumers read `GET /projects`, and archive-aware ones read `GET /projects?includeArchived=true`, under two React Query keys > - A Mine load therefore fetched the same large payload twice (580–590 KB each on our dev instance) > - The two responses differ only by archived rows; the server's active list is exactly the rows where `archivedAt` is null > - This pull request makes every consumer share the `includeArchived=true` query, and active-only consumers filter it with `select` > - The benefit is one project read per page instead of two ## Linked Issues or Issue Description **What existing behavior does this improve?** Duplicate project list reads on page load. **Subsystem affected** Board UI project queries (`ui/src/lib/company-projects-query.ts` and its consumers). **Current behavior** A cold Mine load sends both `/companies/:id/projects` (sidebar) and `/companies/:id/projects?includeArchived=true` (inbox). **Proposed behavior** One `includeArchived=true` read serves both. Active-only consumers apply `selectActiveProjects`. **Reason and benefit** The project list is one of the largest shell payloads. Reading it once saves a large response on every page that has both kinds of consumer. ## What Changed - New `companyProjectsQuery(companyId, { includeArchived? })` in `ui/src/lib/company-projects-query.ts`. It always uses the `includeArchived: true` key and fetch. For active-only use it adds a stable, module-level `select` that keeps rows without `archivedAt`. - 22 files that read the active list now spread `companyProjectsQuery(...)`, and so do the plugin SDK project picker and 16 archive-aware pages that wrote the shared key and fetcher inline. Their `enabled` and other options are unchanged. `OnboardingWizard`'s one imperative read is unchanged. `pages/Inbox.tsx` already reads the same key and fetcher, so it shares the entry without an edit. - Invalidation: project mutations and live events invalidate `queryKeys.projects.all(companyId)`, which matches the shared key. The onboarding wizard invalidated the old active-only key; it now invalidates `projects.all`. - Tests: new `company-projects-query.test.tsx` (one request for both consumers, correct filtering, and one refetch after `projects.all` invalidation). Three existing tests (`CommandPalette`, `CompanyExport`, `GitHubBotManagement`) now seed or expect the shared key. - New browser check `tests/perf/collections/shared-project-list.spec.ts`: after the Mine inbox read completes, the page has sent no active-only project request. ## Verification - `pnpm verify` passes. - `ui`: the consumer suites (`CoordinatorSidebar`, `SidebarProjects`, `SidebarStarredProjects`, `CommandPalette`, `NewIssueDialog`, `AgentWorkPanels`, `CoordinatorNeedsYou`, `IssueDetail`, `ProjectDetail`, `Projects`, `CompanyExport`, `ChatExecutionDefaults`, `GatewaysList`, `Search`, `IssueProperties`, `persisted-query-cache`) pass. - Browser check: `PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1: pnpm exec playwright test --config tests/perf/collections/playwright.config.ts shared-project-list.spec.ts`. It passes on this branch and fails on `master`. ## Risks - A page that needs only active projects now downloads archived rows too. For most companies that is a small difference; the duplicate read it removes is a full copy. - The server does not order either list, and consumers sort for display, so filtering on the client keeps behavior. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with tool use (shell, file edits, browser). 1M-token context window. Extended thinking was off. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../collections/shared-project-list.spec.ts | 28 +++++++ .../components/AgentProjectAccessSettings.tsx | 4 +- ui/src/components/CommandPalette.test.tsx | 2 +- ui/src/components/CommandPalette.tsx | 5 +- ui/src/components/CoordinatorSidebar.tsx | 4 +- ui/src/components/NewIssueDialog.tsx | 5 +- ui/src/components/OnboardingWizard.tsx | 2 +- ui/src/components/SidebarProjects.tsx | 5 +- .../SidebarStarredProjects.production.tsx | 6 +- ui/src/components/SidebarStarredProjects.tsx | 6 +- ui/src/components/WorkspaceFileBrowser.tsx | 5 +- ui/src/components/chat/AgentWorkPanels.tsx | 5 +- .../coordinator/CoordinatorNeedsYou.tsx | 4 +- .../issue-properties/IssueProperties.tsx | 5 +- .../useStandardMarkdownMentionOptions.ts | 5 +- ui/src/lib/company-projects-query.test.tsx | 75 +++++++++++++++++++ ui/src/lib/company-projects-query.ts | 26 +++++++ ui/src/pages/AgentDetail.production.tsx | 5 +- ui/src/pages/AgentDetail.tsx | 5 +- ui/src/pages/Cases.tsx | 5 +- ui/src/pages/CompanyExport.test.tsx | 2 +- ui/src/pages/CompanyExport.tsx | 5 +- ui/src/pages/CoordinatorTasks.tsx | 4 +- ui/src/pages/Dashboard.tsx | 5 +- ui/src/pages/DesignSandboxTasks.tsx | 4 +- ui/src/pages/GoalDetail.tsx | 5 +- ui/src/pages/IssueDetail.tsx | 5 +- ui/src/pages/Issues.tsx | 5 +- ui/src/pages/LegacyInbox.tsx | 5 +- ui/src/pages/ProjectDetail.tsx | 7 +- ui/src/pages/Projects.tsx | 6 +- ui/src/pages/RoutineDetail.production.tsx | 5 +- ui/src/pages/RoutineDetail.tsx | 5 +- ui/src/pages/Routines.production.tsx | 5 +- ui/src/pages/Routines.tsx | 5 +- ui/src/pages/Search.tsx | 5 +- .../pages/apps/chat/ChatExecutionDefaults.tsx | 5 +- ui/src/pages/apps/chat/EmailEndpointSetup.tsx | 5 +- .../apps/chat/GitHubBotManagement.test.tsx | 2 +- ui/src/pages/apps/gateways/GatewayDetail.tsx | 5 +- ui/src/pages/apps/gateways/GatewaysList.tsx | 5 +- .../skills/ImportSkillsFromProjectDialog.tsx | 5 +- ui/src/pages/tools/GatewaysTab.tsx | 5 +- ui/src/pages/tools/ProfilesTab.tsx | 5 +- .../pages/tools/profiles/useProfilesData.ts | 5 +- ui/src/plugins/bridge-init.ts | 8 +- 46 files changed, 213 insertions(+), 122 deletions(-) create mode 100644 tests/perf/collections/shared-project-list.spec.ts create mode 100644 ui/src/lib/company-projects-query.test.tsx create mode 100644 ui/src/lib/company-projects-query.ts diff --git a/tests/perf/collections/shared-project-list.spec.ts b/tests/perf/collections/shared-project-list.spec.ts new file mode 100644 index 00000000000..c6552549b19 --- /dev/null +++ b/tests/perf/collections/shared-project-list.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from "@playwright/test"; +import { collectionFixture, type CollectionFixture } from "./fixture"; + +let fixture: CollectionFixture; +test.beforeAll(async ({ request }) => { + fixture = await collectionFixture(request); +}); + +// The sidebar wants active projects and the inbox wants archived ones too. +// One includeArchived read serves both. +test("Mine reads the company project list once", async ({ page }) => { + const path = `/api/companies/${fixture.company.id}/projects`; + const reads: URL[] = []; + page.on("request", item => { + const url = new URL(item.url()); + if (url.pathname === path) reads.push(url); + }); + const inbox = page.waitForResponse(item => new URL(item.url()).pathname === `/api/companies/${fixture.company.id}/inbox/mine` && item.ok()); + await page.goto(`/${fixture.company.issuePrefix}/issues?view=mine`); + // Mine's own read has completed, so the inbox and the sidebar have both + // mounted their project consumers. + await inbox; + await expect.poll(() => reads.some(url => url.searchParams.get("includeArchived") === "true")).toBe(true); + // The live-updates socket may refresh the shared list once after it opens; + // that is the same read. An active-only read means the payload was fetched twice. + expect(reads.filter(url => url.searchParams.get("includeArchived") !== "true")).toEqual([]); + expect(reads.length).toBeGreaterThanOrEqual(1); +}); diff --git a/ui/src/components/AgentProjectAccessSettings.tsx b/ui/src/components/AgentProjectAccessSettings.tsx index 2320755c85f..e6058b77491 100644 --- a/ui/src/components/AgentProjectAccessSettings.tsx +++ b/ui/src/components/AgentProjectAccessSettings.tsx @@ -1,18 +1,18 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { isUuidLike } from "@paperclipai/shared"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { queryKeys } from "../lib/queryKeys"; import { Link, useSearchParams } from "../lib/router"; import { AllowedProjectsEditor } from "./AllowedProjectsEditor"; import { Button } from "./ui/button"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; export function AgentProjectAccessSettings({ agent }: { agent: { id: string; companyId: string; name: string; urlKey?: string } }) { const client = useQueryClient(); const [params] = useSearchParams(); const fromTask = params.get("fromTask"); const access = useQuery({ queryKey: queryKeys.agents.projectAccess(agent.id), queryFn: () => agentsApi.projectAccess(agent.id) }); - const projects = useQuery({ queryKey: queryKeys.projects.list(agent.companyId), queryFn: () => projectsApi.list(agent.companyId) }); + const projects = useQuery({ ...companyProjectsQuery(agent.companyId) }); if (access.error || projects.error) return

Couldn’t load project access.

; diff --git a/ui/src/components/CommandPalette.test.tsx b/ui/src/components/CommandPalette.test.tsx index 0747407068c..f5c2e42d7cc 100644 --- a/ui/src/components/CommandPalette.test.tsx +++ b/ui/src/components/CommandPalette.test.tsx @@ -346,7 +346,7 @@ describe("CommandPalette", () => { const { root } = renderWithQueryClient(, container, (queryClient) => { // Seed the caches so the already-loaded data is available synchronously — // this harness's flush model doesn't reliably propagate fresh async fetches. - queryClient.setQueryData(queryKeys.projects.list("company-1"), projects); + queryClient.setQueryData(queryKeys.projects.list("company-1", { includeArchived: true }), projects); queryClient.setQueryData(queryKeys.issues.search("company-1", "mob", undefined, 10), [ { id: "i1", identifier: "ENG-9", title: "Fix login" }, ]); diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index 7f29445f678..b346ad3a9ac 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -9,7 +9,6 @@ import { useSidebar } from "../context/SidebarContext"; import { issuesApi } from "../api/issues"; import { authApi } from "../api/auth"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { instanceSettingsApi } from "../api/instanceSettings"; import { queryKeys } from "../lib/queryKeys"; import { @@ -43,6 +42,7 @@ import { parseSearchQuery, type SearchQueryParserContext, } from "../lib/search-query-parser"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; const SEARCH_ALL_VALUE = "__paperclip-search-all__"; @@ -129,8 +129,7 @@ export function CommandPalette() { }); const { data: allProjects = [] } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId && open, }); const projects = useMemo( diff --git a/ui/src/components/CoordinatorSidebar.tsx b/ui/src/components/CoordinatorSidebar.tsx index 37766537f56..d33e596fa21 100644 --- a/ui/src/components/CoordinatorSidebar.tsx +++ b/ui/src/components/CoordinatorSidebar.tsx @@ -5,7 +5,6 @@ import type { Agent, Project } from "@paperclipai/shared"; import { useCoordinatorAttentionCount } from "@/hooks/useCoordinatorAttentionCount"; import { issuesApi } from "@/api/issues"; import { instanceSettingsApi } from "@/api/instanceSettings"; -import { projectsApi } from "@/api/projects"; import { useAgentChatNavigation } from "@/hooks/useAgentChatNavigation"; import { useRecentTasks } from "@/hooks/useRecentTasks"; import { usePinnedNavigation } from "@/hooks/usePinnedNavigation"; @@ -27,6 +26,7 @@ import { taskStatusIconProps } from "@/lib/task-status-presentation"; import { AgentAvatar } from "./AgentAvatar"; import { agentChatPinKey } from "./AgentChatPin"; import { PinnedNavigationList, type PinnedNavigationItem } from "./PinnedNavigationList"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; const moreDestinations = [ { label: "Search", to: "/search", icon: Search }, @@ -122,7 +122,7 @@ export function CoordinatorSidebar({ children }: { children?: ReactNode }) { const { openNewIssue } = useDialogActions(); const location = useLocation(); const attentionCount = useCoordinatorAttentionCount(companyId); - const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(companyId!), queryFn: () => projectsApi.list(companyId!), enabled: !!companyId }); + const { data: projects } = useQuery({ ...companyProjectsQuery(companyId!), enabled: !!companyId }); // Only project IDs and visit times are kept in memory. Labels and task lists // always come from the authorized API, scoped to the current person/company. const scope = `${companyId}:${userId}`; diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 82886dc1b37..62bc012474d 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { usePrimaryAgentPresentation } from "./primary-agent/PrimaryAgentPresentation"; import { useWorkspaceIsolationControls } from "@/hooks/useWorkspaceIsolationControls"; import { useState, useEffect, useRef, useCallback, useMemo, type CSSProperties } from "react"; @@ -10,7 +11,6 @@ import { executionWorkspacesApi } from "../api/execution-workspaces"; import { issuesApi } from "../api/issues"; import { MissingUserSecretsBanner } from "../pages/secrets/MissingUserSecretsBanner"; import { instanceSettingsApi } from "../api/instanceSettings"; -import { projectsApi } from "../api/projects"; import { agentsApi } from "../api/agents"; import { accessApi } from "../api/access"; import { authApi } from "../api/auth"; @@ -517,8 +517,7 @@ function AccountNewIssueDialog({ ignoreDefaults }: { ignoreDefaults: boolean }) }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(effectiveCompanyId!), - queryFn: () => projectsApi.list(effectiveCompanyId!), + ...companyProjectsQuery(effectiveCompanyId!), enabled: !!effectiveCompanyId && newIssueOpen, }); const { data: mentionIssues } = useQuery({ diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index c2ba778a69e..a5e1eb0f383 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -1920,7 +1920,7 @@ function OnboardingWizardInner({ ); projectId = project.id; queryClient.invalidateQueries({ - queryKey: queryKeys.projects.list(createdCompanyId) + queryKey: queryKeys.projects.all(createdCompanyId) }); } if (stillTheSameCompany(createdCompanyId)) setCreatedProjectId(projectId); diff --git a/ui/src/components/SidebarProjects.tsx b/ui/src/components/SidebarProjects.tsx index 7e1c2a7e2d7..ce404242820 100644 --- a/ui/src/components/SidebarProjects.tsx +++ b/ui/src/components/SidebarProjects.tsx @@ -16,7 +16,6 @@ import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { useSidebar } from "../context/SidebarContext"; import { authApi } from "../api/auth"; -import { projectsApi } from "../api/projects"; import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll"; import { queryKeys } from "../lib/queryKeys"; import { cn, projectRouteRef, SIDEBAR_RAIL_HIDDEN_LABEL } from "../lib/utils"; @@ -45,6 +44,7 @@ import { writeProjectSortMode, } from "../lib/project-order"; import type { Project } from "@paperclipai/shared"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; type ProjectSidebarSlot = ReturnType["slots"][number]; @@ -263,8 +263,7 @@ export function SidebarProjects() { const location = useLocation(); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const membershipsQuery = useResourceMemberships(selectedCompanyId); diff --git a/ui/src/components/SidebarStarredProjects.production.tsx b/ui/src/components/SidebarStarredProjects.production.tsx index f5f26fed4f9..cdf271c1e98 100644 --- a/ui/src/components/SidebarStarredProjects.production.tsx +++ b/ui/src/components/SidebarStarredProjects.production.tsx @@ -4,9 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { Lock, Loader2, LogOut, MoreHorizontal, Star } from "lucide-react"; import { useCompany } from "../context/CompanyContext"; import { useSidebar } from "../context/SidebarContext"; -import { projectsApi } from "../api/projects"; import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll"; -import { queryKeys } from "../lib/queryKeys"; import { cn, projectRouteRef, SIDEBAR_RAIL_HIDDEN_LABEL } from "../lib/utils"; import { isStarred, @@ -27,6 +25,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { Project } from "@paperclipai/shared"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; // Sidebar star reveals with the row's own group, not the shared unnamed group. const STAR_ROW_REVEAL = @@ -46,8 +45,7 @@ export function SidebarStarredProjects() { const location = useLocation(); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const membershipsQuery = useResourceMemberships(selectedCompanyId); diff --git a/ui/src/components/SidebarStarredProjects.tsx b/ui/src/components/SidebarStarredProjects.tsx index 97e724b7e5e..c35b4373c05 100644 --- a/ui/src/components/SidebarStarredProjects.tsx +++ b/ui/src/components/SidebarStarredProjects.tsx @@ -4,9 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { Lock, Loader2, LogOut, MoreHorizontal, Star } from "lucide-react"; import { useCompany } from "../context/CompanyContext"; import { useSidebar } from "../context/SidebarContext"; -import { projectsApi } from "../api/projects"; import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll"; -import { queryKeys } from "../lib/queryKeys"; import { cn, projectRouteRef, SIDEBAR_RAIL_HIDDEN_LABEL } from "../lib/utils"; import { isStarred, @@ -27,6 +25,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { Project } from "@paperclipai/shared"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; // Sidebar star reveals with the row's own group, not the shared unnamed group. const STAR_ROW_REVEAL = @@ -46,8 +45,7 @@ export function SidebarStarredProjects() { const location = useLocation(); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const membershipsQuery = useResourceMemberships(selectedCompanyId); diff --git a/ui/src/components/WorkspaceFileBrowser.tsx b/ui/src/components/WorkspaceFileBrowser.tsx index 305f4d5c5ad..12d4013f601 100644 --- a/ui/src/components/WorkspaceFileBrowser.tsx +++ b/ui/src/components/WorkspaceFileBrowser.tsx @@ -13,7 +13,6 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { cn, formatDateTime } from "@/lib/utils"; import { fileResourcesApi } from "@/api/file-resources"; -import { projectsApi } from "@/api/projects"; import { ApiError } from "@/api/client"; import { describeError } from "@/api/errors"; import { queryViewKind } from "@/components/QueryView"; @@ -27,6 +26,7 @@ import type { WorkspaceFileListMode, WorkspaceFileSelector, } from "@paperclipai/shared"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; type BrowserSource = "current" | "other"; @@ -539,8 +539,7 @@ export function WorkspaceFileBrowser({ const scrollContainerRef = useRef(null); const projectsQuery = useQuery({ - queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "__none__"], - queryFn: () => projectsApi.list(companyId!), + ...companyProjectsQuery(companyId ?? ""), enabled: source === "other" && !!companyId, staleTime: 30_000, }); diff --git a/ui/src/components/chat/AgentWorkPanels.tsx b/ui/src/components/chat/AgentWorkPanels.tsx index 1f7b91a37c4..51472a63589 100644 --- a/ui/src/components/chat/AgentWorkPanels.tsx +++ b/ui/src/components/chat/AgentWorkPanels.tsx @@ -4,7 +4,6 @@ import { ArrowUpDown, File, FileText, Image, Search, Video } from "lucide-react" import type { CompanyArtifact, Issue } from "@paperclipai/shared"; import { artifactsApi } from "@/api/artifacts"; import { issuesApi } from "@/api/issues"; -import { projectsApi } from "@/api/projects"; import { IssueFiltersPopover } from "@/components/IssueFiltersPopover"; import { StatusIcon } from "@/components/StatusIcon"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; @@ -22,6 +21,7 @@ import { queryKeys } from "@/lib/queryKeys"; import { Link } from "@/lib/router"; import { cn, formatDate, relativeTime } from "@/lib/utils"; import { taskStatusIconProps } from "@/lib/task-status-presentation"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; /** * The agent-scoped side panels shown beside an agent chat: every task the @@ -131,8 +131,7 @@ export function AgentTasksPanel({ }); const tasksView = useQueryView(tasksQuery); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), + ...companyProjectsQuery(companyId), }); const visible = useMemo(() => { diff --git a/ui/src/components/coordinator/CoordinatorNeedsYou.tsx b/ui/src/components/coordinator/CoordinatorNeedsYou.tsx index 447adc8b27f..ce3faa9bd37 100644 --- a/ui/src/components/coordinator/CoordinatorNeedsYou.tsx +++ b/ui/src/components/coordinator/CoordinatorNeedsYou.tsx @@ -7,7 +7,6 @@ import { LayoutGroup, motion, useReducedMotion } from "motion/react"; import type { Agent, AttentionItem, DecisionHistoryItem } from "@paperclipai/shared"; import { attentionApi } from "@/api/attention"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { usePanel } from "@/context/PanelContext"; @@ -30,6 +29,7 @@ import { parseCssTimeMs } from "@/components/task-chat/motion-tokens"; import { readIssueReviewPolicyMetadata } from "@/lib/review-policy"; import { CoordinatorDecisionReceipt as Receipt } from "./CoordinatorDecisionReceipt"; import { CoordinatorTaskMotion } from "./CoordinatorTaskCommitment"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; type Group = "none" | "task" | "agent" | "project"; @@ -41,7 +41,7 @@ function HistoryPanel({ companyId, agents, onClose, mobile = false }: { companyI const [projectId, setProjectId] = useState(""); const [groupBy, setGroupBy] = useState("none"); const [sort, setSort] = useState<"newest" | "oldest">("newest"); - const projects = useQuery({ queryKey: queryKeys.projects.list(companyId), queryFn: () => projectsApi.list(companyId) }); + const projects = useQuery({ ...companyProjectsQuery(companyId) }); const history = useInfiniteQuery({ queryKey: [...queryKeys.attention(companyId), "history", { search, outcome, agentId, projectId, sort }], queryFn: ({ pageParam }) => attentionApi.history(companyId, { search, outcome, agentId, projectId, sort, cursor: pageParam, limit: 40 }), diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index 2ca2e7b03de..705fedb32e1 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { isLockedIssueStub } from "@/components/LockedIssueChip"; import { IssuePullRequestLinks } from "../IssuePullRequestLinks"; import { useIssueWorkProducts } from "../../hooks/useIssueWorkProducts"; @@ -32,7 +33,6 @@ import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument"; import { useIssueDocuments } from "@/hooks/useIssueDocuments"; import { useStreamlinedUiEnabled } from "@/hooks/useStreamlinedUiEnabled"; import { selectArtifactAttachments } from "@/lib/issue-artifacts"; -import { projectsApi } from "../../api/projects"; import { useCompany } from "../../context/CompanyContext"; import { useSidebar } from "../../context/SidebarContext"; import { queryKeys } from "../../lib/queryKeys"; @@ -452,8 +452,7 @@ export function IssueProperties({ enabled: !!companyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(companyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(companyId!, { includeArchived: true }), + ...companyProjectsQuery(companyId!, { includeArchived: true }), enabled: !!companyId, }); const activeProjects = useMemo( diff --git a/ui/src/hooks/useStandardMarkdownMentionOptions.ts b/ui/src/hooks/useStandardMarkdownMentionOptions.ts index b3c7ef462a2..ee1505d31af 100644 --- a/ui/src/hooks/useStandardMarkdownMentionOptions.ts +++ b/ui/src/hooks/useStandardMarkdownMentionOptions.ts @@ -2,10 +2,10 @@ import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { agentsApi } from "../api/agents"; import { accessApi } from "../api/access"; -import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { buildMarkdownMentionOptions } from "../lib/company-members"; import { queryKeys } from "../lib/queryKeys"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; type MarkdownMentionInputs = Parameters[0]; @@ -25,8 +25,7 @@ export function useStandardMarkdownMentionOptions(args: StandardMarkdownMentionO enabled: enabled && args.agents === undefined, }); const projectsQuery = useQuery({ - queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "standard-mentions", "none"], - queryFn: () => projectsApi.list(companyId!), + ...companyProjectsQuery(companyId ?? ""), enabled: enabled && args.projects === undefined, }); const usersQuery = useQuery({ diff --git a/ui/src/lib/company-projects-query.test.tsx b/ui/src/lib/company-projects-query.test.tsx new file mode 100644 index 00000000000..517c7de1c10 --- /dev/null +++ b/ui/src/lib/company-projects-query.test.tsx @@ -0,0 +1,75 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ list: vi.fn() })); +vi.mock("@/api/projects", () => ({ projectsApi: { list: mocks.list } })); + +import { companyProjectsQuery } from "./company-projects-query"; +import { queryKeys } from "./queryKeys"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +const active = { id: "p-active", name: "Active", archivedAt: null }; +const archived = { id: "p-archived", name: "Archived", archivedAt: "2026-11-01T00:00:00.000Z" }; + +describe("companyProjectsQuery", () => { + let container: HTMLDivElement; + let root: ReturnType; + let queryClient: QueryClient; + + beforeEach(() => { + mocks.list.mockReset().mockResolvedValue([active, archived]); + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + queryClient.clear(); + }); + + it("serves active-only and archive-inclusive consumers from one request", async () => { + const seen: Record = {}; + function Sidebar() { + const { data } = useQuery(companyProjectsQuery("company-1")); + seen.sidebar = data?.map((project) => project.id); + return null; + } + function Inbox() { + const { data } = useQuery(companyProjectsQuery("company-1", { includeArchived: true })); + seen.inbox = data?.map((project) => project.id); + return null; + } + + act(() => { + root.render( + + + + , + ); + }); + await vi.waitFor(() => { + expect(seen.sidebar).toEqual(["p-active"]); + expect(seen.inbox).toEqual(["p-active", "p-archived"]); + }); + expect(mocks.list).toHaveBeenCalledTimes(1); + expect(mocks.list).toHaveBeenCalledWith("company-1", { includeArchived: true }); + + // Project mutations invalidate the company prefix; both views refresh from one read. + mocks.list.mockResolvedValue([active, { ...archived, archivedAt: null }]); + await act(async () => { + await queryClient.invalidateQueries({ queryKey: queryKeys.projects.all("company-1") }); + }); + await vi.waitFor(() => expect(seen.sidebar).toEqual(["p-active", "p-archived"])); + expect(mocks.list).toHaveBeenCalledTimes(2); + }); +}); diff --git a/ui/src/lib/company-projects-query.ts b/ui/src/lib/company-projects-query.ts new file mode 100644 index 00000000000..10065e77310 --- /dev/null +++ b/ui/src/lib/company-projects-query.ts @@ -0,0 +1,26 @@ +import { queryOptions } from "@tanstack/react-query"; +import type { Project } from "@paperclipai/shared"; +import { projectsApi } from "@/api/projects"; +import { queryKeys } from "@/lib/queryKeys"; + +/** Active projects are exactly those the server would return without `includeArchived`. */ +export function selectActiveProjects(projects: Project[]): Project[] { + return projects.filter((project) => !project.archivedAt); +} + +/** + * The company project list. Every consumer shares the single + * `includeArchived=true` read; active-only consumers filter it with `select`. + * Two lists under separate keys used to fetch the same large payload twice on + * one page (for example the sidebar and the Mine inbox). + * `queryKeys.projects.all(companyId)` invalidation still reaches it. + */ +export function companyProjectsQuery(companyId: string, opts: { includeArchived?: boolean } = {}) { + const base = { + queryKey: queryKeys.projects.list(companyId, { includeArchived: true }), + queryFn: () => projectsApi.list(companyId, { includeArchived: true }), + }; + return opts.includeArchived + ? queryOptions(base) + : queryOptions({ ...base, select: selectActiveProjects }); +} diff --git a/ui/src/pages/AgentDetail.production.tsx b/ui/src/pages/AgentDetail.production.tsx index c6fc9a77353..b4531da983c 100644 --- a/ui/src/pages/AgentDetail.production.tsx +++ b/ui/src/pages/AgentDetail.production.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { describeError } from "../api/errors"; import { useSelectedHeartbeatRun } from "../hooks/useSelectedHeartbeatRun"; import { AgentCharacter } from "../components/AgentCharacter"; @@ -24,7 +25,6 @@ import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { activityApi } from "../api/activity"; import { accessApi } from "../api/access"; import { issuesApi } from "../api/issues"; -import { projectsApi } from "../api/projects"; import { usePanel } from "../context/PanelContext"; import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; @@ -1646,8 +1646,7 @@ export function ConfigurationTab({ const lowTrustSelected = getTrustPreset(agent.permissions) === "low_trust_review"; const { data: boundaryProjects, isLoading: boundaryProjectsLoading } = useQuery({ - queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "__low-trust-disabled"], - queryFn: () => projectsApi.list(companyId!), + ...companyProjectsQuery(companyId ?? ""), enabled: Boolean(companyId && lowTrustSelected) && content === "configuration", }); diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 31a2e2966c1..76a25681c1d 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { describeError } from "../api/errors"; import { useSelectedHeartbeatRun } from "../hooks/useSelectedHeartbeatRun"; import { grokBotApi } from "@/api/grokBot"; @@ -30,7 +31,6 @@ import { ApiError } from "../api/client"; import { activityApi } from "../api/activity"; import { accessApi } from "../api/access"; import { issuesApi } from "../api/issues"; -import { projectsApi } from "../api/projects"; import { usePanel } from "../context/PanelContext"; import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; @@ -1659,8 +1659,7 @@ export function ConfigurationTab({ const lowTrustSelected = getTrustPreset(agent.permissions) === "low_trust_review"; const { data: boundaryProjects, isLoading: boundaryProjectsLoading } = useQuery({ - queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "__low-trust-disabled"], - queryFn: () => projectsApi.list(companyId!), + ...companyProjectsQuery(companyId ?? ""), enabled: Boolean(companyId && lowTrustSelected) && content === "permissions", }); diff --git a/ui/src/pages/Cases.tsx b/ui/src/pages/Cases.tsx index ebe45b99c4c..57b024d2c25 100644 --- a/ui/src/pages/Cases.tsx +++ b/ui/src/pages/Cases.tsx @@ -1,9 +1,9 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useQueryView } from "@/components/QueryView"; import { useEffect, useLayoutEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { FolderOpen, Search } from "lucide-react"; import { casesApi, CASE_STATUSES, type ListCasesParams } from "@/api/cases"; -import { projectsApi } from "@/api/projects"; import { CaseListRow } from "@/components/cases/CaseListRow"; import { CaseListControls } from "@/components/cases/CaseListControls"; import { CollectionToolbar } from "@/components/CollectionToolbar"; @@ -86,8 +86,7 @@ export function Cases() { }); const typesQueryView = useQueryView(typesQuery); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId ?? "", { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId ?? "", { includeArchived: true }), enabled: !!selectedCompanyId, }); const projectsQueryView = useQueryView(projectsQuery); diff --git a/ui/src/pages/CompanyExport.test.tsx b/ui/src/pages/CompanyExport.test.tsx index 50e520bfb37..f9b74e94678 100644 --- a/ui/src/pages/CompanyExport.test.tsx +++ b/ui/src/pages/CompanyExport.test.tsx @@ -244,7 +244,7 @@ describe("CompanyExport", () => { expect(container.textContent).not.toContain("Prepare export preview"); expect(mockAuthApi.getSession).toHaveBeenCalledTimes(1); expect(mockAgentsApi.list).toHaveBeenCalledWith("company-1"); - expect(mockProjectsApi.list).toHaveBeenCalledWith("company-1"); + expect(mockProjectsApi.list).toHaveBeenCalledWith("company-1", { includeArchived: true }); expect(mockCompaniesApi.exportPreview).toHaveBeenCalledTimes(1); expect(mockCompaniesApi.exportPreview.mock.calls[0]?.[1]).toMatchObject({ include: { company: true, agents: true, projects: true, issues: false, skills: true }, diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 9e353a91b98..d0d63bcfb66 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -15,7 +15,6 @@ import { useToastActions } from "../context/ToastContext"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; import { companiesApi } from "../api/companies"; -import { projectsApi } from "../api/projects"; import { Button } from "@/components/ui/button"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; @@ -59,6 +58,7 @@ import { FileTree, } from "../components/FileTree"; import { describeError } from "@/api/errors"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; /** * Extract the set of agent/project/task slugs that are "checked" based on @@ -643,8 +643,7 @@ export function CompanyExport() { enabled: !!selectedCompanyId, }); const { data: projects = [] } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: fidelityReport } = useQuery({ diff --git a/ui/src/pages/CoordinatorTasks.tsx b/ui/src/pages/CoordinatorTasks.tsx index 151668e54d5..6d66fdaf8e3 100644 --- a/ui/src/pages/CoordinatorTasks.tsx +++ b/ui/src/pages/CoordinatorTasks.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { readTaskListSortParams, taskListOrderToIssueSort, type TaskListSort } from "@/lib/task-list"; import { useDeferredValue, useEffect, useRef, useState, type ReactNode } from "react"; import { useWarmTaskDetailCode } from "@/hooks/useWarmTaskDetailCode"; @@ -8,7 +9,6 @@ import { agentsApi } from "@/api/agents"; import { attentionApi } from "@/api/attention"; import { heartbeatsApi } from "@/api/heartbeats"; import { issuesApi } from "@/api/issues"; -import { projectsApi } from "@/api/projects"; import { EmptyState } from "@/components/EmptyState"; import { KanbanBoard } from "@/components/KanbanBoard"; import { TaskList, type TaskListItem } from "@/components/TaskList"; @@ -150,7 +150,7 @@ export function CoordinatorTasks({ viewControl }: { viewControl?: ReactNode } = enabled: !!companyId, })) }); const agents = useQuery({ queryKey: queryKeys.agents.list(companyId!), queryFn: () => agentsApi.list(companyId!), enabled: !!companyId }); - const projects = useQuery({ queryKey: queryKeys.projects.list(companyId!, { includeArchived: true }), queryFn: () => projectsApi.list(companyId!, { includeArchived: true }), enabled: !!companyId }); + const projects = useQuery({ ...companyProjectsQuery(companyId!, { includeArchived: true }), enabled: !!companyId }); const directory = useQuery({ queryKey: queryKeys.access.companyUserDirectory(companyId!), queryFn: () => accessApi.listUserDirectory(companyId!), enabled: !!companyId }); const labels = useQuery({ queryKey: queryKeys.issues.labels(companyId!), queryFn: () => issuesApi.listLabels(companyId!), enabled: !!companyId }); const sharedLiveRuns = useSharedPollingQuery({ companyId, resourceKey: "live-runs", queryKey: queryKeys.liveRuns(companyId!), enabled: !!companyId, refetchInterval: false, leaderOnly: true }); diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index 23130b0198e..0d9a71537d9 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { AgentIdentity } from "../components/AgentIdentity"; import { TaskSummaryList } from "../components/TaskSummaryList"; import { useDesignSandbox } from "../context/DesignSandboxContext"; @@ -13,7 +14,6 @@ import { activityApi } from "../api/activity"; import { accessApi } from "../api/access"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { buildCompanyUserProfileMap } from "../lib/company-members"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; @@ -202,8 +202,7 @@ export function Dashboard() { }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); diff --git a/ui/src/pages/DesignSandboxTasks.tsx b/ui/src/pages/DesignSandboxTasks.tsx index 3c56c479cf4..eb7ba9c874e 100644 --- a/ui/src/pages/DesignSandboxTasks.tsx +++ b/ui/src/pages/DesignSandboxTasks.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useEffect } from "react"; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ListChecks, SearchX } from "lucide-react"; @@ -5,7 +6,6 @@ import { accessApi } from "@/api/access"; import { agentsApi } from "@/api/agents"; import { useAccountIdentity } from "@/api/companies-query"; import { issuesApi } from "@/api/issues"; -import { projectsApi } from "@/api/projects"; import { EmptyState } from "@/components/EmptyState"; import { KanbanBoard } from "@/components/KanbanBoard"; import { TaskList, type TaskListItem } from "@/components/TaskList"; @@ -45,7 +45,7 @@ export function DesignSandboxTasks() { enabled: !!companyId, }); const agents = useQuery({ queryKey: queryKeys.agents.list(companyId!), queryFn: () => agentsApi.list(companyId!), enabled: !!companyId }); - const projects = useQuery({ queryKey: queryKeys.projects.list(companyId!, { includeArchived: true }), queryFn: () => projectsApi.list(companyId!, { includeArchived: true }), enabled: !!companyId }); + const projects = useQuery({ ...companyProjectsQuery(companyId!, { includeArchived: true }), enabled: !!companyId }); const directory = useQuery({ queryKey: queryKeys.access.companyUserDirectory(companyId!), queryFn: () => accessApi.listUserDirectory(companyId!), enabled: !!companyId }); const labels = useQuery({ queryKey: queryKeys.issues.labels(companyId!), queryFn: () => issuesApi.listLabels(companyId!), enabled: !!companyId }); // The ordinary organization list does not include viewer read context. diff --git a/ui/src/pages/GoalDetail.tsx b/ui/src/pages/GoalDetail.tsx index 5bcf7a9bca6..edfce1697e9 100644 --- a/ui/src/pages/GoalDetail.tsx +++ b/ui/src/pages/GoalDetail.tsx @@ -1,8 +1,8 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useEffect } from "react"; import { useParams } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { goalsApi } from "../api/goals"; -import { projectsApi } from "../api/projects"; import { assetsApi } from "../api/assets"; import { usePanel } from "../context/PanelContext"; import { useCompany } from "../context/CompanyContext"; @@ -71,8 +71,7 @@ export function GoalDetail() { }); const { data: allProjects } = useQuery({ - queryKey: queryKeys.projects.list(resolvedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(resolvedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(resolvedCompanyId!, { includeArchived: true }), enabled: !!resolvedCompanyId }); diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index e86eaf9eaac..578791fe7e3 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { NativeControllerContainmentRecovery } from "../components/IssueRecoveryActionCard"; import { useIssueIdBatches } from "@/hooks/useIssueIdBatches"; import { TaskChatLoadingShell } from "@/components/task-chat/TaskChatLoadingShell"; @@ -103,7 +104,6 @@ import { } from "../lib/recovery-reconcile"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; -import { projectsApi } from "../api/projects"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; @@ -3716,8 +3716,7 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS }); const { data: projects = EMPTY_PROJECTS } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index 0ebf1ceff1f..10a24fdb84a 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { useShowAgentCreatedTasks } from "../hooks/useShowAgentCreatedTasks"; import { useCoordinatorWorkspaceEnabled } from "../hooks/useCoordinatorWorkspaceEnabled"; @@ -10,7 +11,6 @@ import { useLocation, useNavigate, useSearchParams } from "@/lib/router"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { heartbeatsApi } from "../api/heartbeats"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -236,8 +236,7 @@ function OrganizationIssues({ }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); diff --git a/ui/src/pages/LegacyInbox.tsx b/ui/src/pages/LegacyInbox.tsx index 6bfde7ff373..8bc2a5a2e7a 100644 --- a/ui/src/pages/LegacyInbox.tsx +++ b/ui/src/pages/LegacyInbox.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { describeError } from "../api/errors"; import { useWarmTaskDetailCode } from "@/hooks/useWarmTaskDetailCode"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -16,7 +17,6 @@ import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { instanceSettingsApi } from "../api/instanceSettings"; -import { projectsApi } from "../api/projects"; import { BLOCKED_GROUP_OPTIONS, BLOCKED_SORT_OPTIONS, @@ -754,8 +754,7 @@ export function Inbox() { }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: labels } = useQuery({ diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index dbcc211f8aa..f22f9e207b9 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -48,6 +48,7 @@ import { useResourceMemberships, } from "../hooks/useResourceMemberships"; import { useUpdateIssueMutation } from "../hooks/useUpdateIssueMutation"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; /* ── Top-level tab types ── */ @@ -216,8 +217,7 @@ function ProjectIssuesList({ projectId, companyId }: { projectId: string; compan }); usePublishSharedQueryData(sharedLiveRuns, liveRuns, liveRunsUpdatedAt); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), + ...companyProjectsQuery(companyId), enabled: !!companyId, }); @@ -270,8 +270,7 @@ function ProjectPluginOperationsList({ enabled: !!companyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), + ...companyProjectsQuery(companyId), enabled: !!companyId, }); const liveRunsQueryKey = queryKeys.liveRuns(companyId); diff --git a/ui/src/pages/Projects.tsx b/ui/src/pages/Projects.tsx index 6f83abebb8a..3714cdddf9b 100644 --- a/ui/src/pages/Projects.tsx +++ b/ui/src/pages/Projects.tsx @@ -1,11 +1,9 @@ import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { Project } from "@paperclipai/shared"; -import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; -import { queryKeys } from "../lib/queryKeys"; import { EntityRow } from "../components/EntityRow"; import { ProjectTile } from "../components/ProjectTile"; import { StatusBadge } from "../components/StatusBadge"; @@ -26,6 +24,7 @@ import { ProjectsToolbar, type ProjectSortField, type ProjectSortDir } from "@/c import { Hexagon } from "lucide-react"; import { Lock } from "lucide-react"; import { Card } from "@/components/ui/card"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; function compareProjectNames(left: Project, right: Project) { const nameDiff = left.name.localeCompare(right.name, undefined, { sensitivity: "base" }); @@ -81,8 +80,7 @@ export function Projects() { }, [setBreadcrumbs]); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: allProjects, isLoading } = projectsQuery; diff --git a/ui/src/pages/RoutineDetail.production.tsx b/ui/src/pages/RoutineDetail.production.tsx index b1228a78d1a..a16ca2a9060 100644 --- a/ui/src/pages/RoutineDetail.production.tsx +++ b/ui/src/pages/RoutineDetail.production.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Navigate, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -13,7 +14,6 @@ import { secretsApi } from "../api/secrets"; import { type RoutineHistoryDirtyFieldDescriptor } from "../components/RoutineHistoryTab"; import { heartbeatsApi } from "../api/heartbeats"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { accessApi } from "../api/access"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -241,8 +241,7 @@ export function RoutineDetail() { enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ diff --git a/ui/src/pages/RoutineDetail.tsx b/ui/src/pages/RoutineDetail.tsx index bf1e3aa1f2f..f4a81ed7d7b 100644 --- a/ui/src/pages/RoutineDetail.tsx +++ b/ui/src/pages/RoutineDetail.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Navigate, useNavigate, useParams, useSearchParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -13,7 +14,6 @@ import { secretsApi } from "../api/secrets"; import { type RoutineHistoryDirtyFieldDescriptor } from "../components/RoutineHistoryTab"; import { heartbeatsApi } from "../api/heartbeats"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { accessApi } from "../api/access"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -223,8 +223,7 @@ export function RoutineDetail() { enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ diff --git a/ui/src/pages/Routines.production.tsx b/ui/src/pages/Routines.production.tsx index 31936d21e5a..71e96ae218d 100644 --- a/ui/src/pages/Routines.production.tsx +++ b/ui/src/pages/Routines.production.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { AgentAvatar } from "@/components/AgentAvatar"; import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -6,7 +7,6 @@ import { ArrowUpDown, Check, ChevronDown, ChevronRight, Layers, Plus, Repeat } f import { routinesApi } from "../api/routines"; import { foldersApi } from "../api/folders"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { issuesApi } from "../api/issues"; import { heartbeatsApi } from "../api/heartbeats"; import { accessApi } from "../api/access"; @@ -395,8 +395,7 @@ export function Routines() { enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ diff --git a/ui/src/pages/Routines.tsx b/ui/src/pages/Routines.tsx index 00154a1a5eb..296558ca690 100644 --- a/ui/src/pages/Routines.tsx +++ b/ui/src/pages/Routines.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { AgentAvatar } from "@/components/AgentAvatar"; import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -6,7 +7,6 @@ import { ChevronDown, ChevronRight, Plus, Repeat } from "lucide-react"; import { routinesApi } from "../api/routines"; import { foldersApi } from "../api/folders"; import { agentsApi } from "../api/agents"; -import { projectsApi } from "../api/projects"; import { issuesApi } from "../api/issues"; import { heartbeatsApi } from "../api/heartbeats"; import { accessApi } from "../api/access"; @@ -391,8 +391,7 @@ export function Routines() { enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ diff --git a/ui/src/pages/Search.tsx b/ui/src/pages/Search.tsx index 844eacab19d..fce7655830a 100644 --- a/ui/src/pages/Search.tsx +++ b/ui/src/pages/Search.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Search as SearchIcon, AlertTriangle, FileQuestion, Plus, X } from "lucide-react"; @@ -25,7 +26,6 @@ import { useQueryView } from "../components/QueryView"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; import { issuesApi } from "../api/issues"; -import { projectsApi } from "../api/projects"; import { queryKeys } from "../lib/queryKeys"; import { loadRecentSearches, pushRecentSearch } from "../lib/recent-searches"; import { PageTabBar, type PageTabItem } from "../components/PageTabBar"; @@ -187,8 +187,7 @@ export function Search() { }); const { data: projects = [] } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!, { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); diff --git a/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx b/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx index 7e3ccd4ad7b..51b5982cdb2 100644 --- a/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx +++ b/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx @@ -1,11 +1,10 @@ import { useEffect, useId, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { ChatExecutionDefaults as Defaults, TaskWorkspaceSelection } from "@paperclipai/shared"; -import { projectsApi } from "@/api/projects"; import { executionWorkspacesApi } from "@/api/execution-workspaces"; import { Button } from "@/components/ui/button"; import { NativeSelect } from "@/components/ui/select"; -import { queryKeys } from "@/lib/queryKeys"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; function selectionKey(value: TaskWorkspaceSelection | null | undefined): string { if (value === undefined) return "inherit"; @@ -26,7 +25,7 @@ export function ChatExecutionDefaults({ companyId, value, resource = false, onSa const [pending, setPending] = useState(false); const [error, setError] = useState(null); const [savedFrom, setSavedFrom] = useState(null); - const projects = useQuery({ queryKey: queryKeys.projects.list(companyId), queryFn: () => projectsApi.list(companyId) }); + const projects = useQuery({ ...companyProjectsQuery(companyId) }); const workspaces = useQuery({ queryKey: ["channel-workspace-choices", companyId], queryFn: () => executionWorkspacesApi.listSummaries(companyId, { selectableForTask: true }), diff --git a/ui/src/pages/apps/chat/EmailEndpointSetup.tsx b/ui/src/pages/apps/chat/EmailEndpointSetup.tsx index a6ee9a0b9c6..b4aeca4eaf6 100644 --- a/ui/src/pages/apps/chat/EmailEndpointSetup.tsx +++ b/ui/src/pages/apps/chat/EmailEndpointSetup.tsx @@ -20,7 +20,6 @@ import { useCompany } from "@/context/CompanyContext"; import { useNavigate, useSearchParams, Link } from "@/lib/router"; import { agentsApi } from "@/api/agents"; import { issuesApi } from "@/api/issues"; -import { projectsApi } from "@/api/projects"; import { toolsApi } from "@/api/tools"; import { emailApi } from "@/api/email"; import { Button } from "@/components/ui/button"; @@ -51,6 +50,7 @@ import type { AgentPermissions, EmailEndpointSummary, } from "@paperclipai/shared"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; const selectClass = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm"; @@ -134,8 +134,7 @@ function EmailEndpointSetupForm({ companyId }: { companyId: string }) { const agents = useQuery({ queryKey: queryKeys.agents.list(companyId), queryFn: () => agentsApi.list(companyId), enabled: !!companyId }); const agentsView = useQueryView(agents); - const projects = useQuery({ queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), enabled: !!companyId && trustOpen }); + const projects = useQuery({ ...companyProjectsQuery(companyId), enabled: !!companyId && trustOpen }); const projectsView = useQueryView(projects); const boundaryIssues = useQuery({ queryKey: ["email-boundary-issues", companyId], queryFn: () => issuesApi.list(companyId), enabled: !!companyId && trustOpen }); diff --git a/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx b/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx index f3a4a1c911c..592bf3314f0 100644 --- a/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx +++ b/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx @@ -324,7 +324,7 @@ describe("GitHub bot management", () => { const editor = container.querySelector('[role="region"] form')!; const [project, workspace] = [...editor.querySelectorAll("select")]; await vi.waitFor(() => expect(workspace.disabled).toBe(false)); - expect(mocks.projects).toHaveBeenCalledWith("company"); + expect(mocks.projects).toHaveBeenCalledWith("company", { includeArchived: true }); expect(mocks.workspaces).toHaveBeenCalledWith("company", { selectableForTask: true }); expect(project.value).toBe("inherit"); expect(workspace.value).toBe("inherit"); diff --git a/ui/src/pages/apps/gateways/GatewayDetail.tsx b/ui/src/pages/apps/gateways/GatewayDetail.tsx index 67291e4abd6..369fef76603 100644 --- a/ui/src/pages/apps/gateways/GatewayDetail.tsx +++ b/ui/src/pages/apps/gateways/GatewayDetail.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Pencil, Send } from "lucide-react"; @@ -9,7 +10,6 @@ import { useToast } from "@/context/ToastContext"; import { queryKeys } from "@/lib/queryKeys"; import { toolsApi } from "@/api/tools"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -66,8 +66,7 @@ export function GatewayDetail() { enabled: !!selectedCompanyId, }); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId ?? "__none__", { includeArchived: true }), - queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), + ...companyProjectsQuery(selectedCompanyId ?? "__none__", { includeArchived: true }), enabled: !!selectedCompanyId, }); const gatewaysView = useQueryView(gatewaysQuery); diff --git a/ui/src/pages/apps/gateways/GatewaysList.tsx b/ui/src/pages/apps/gateways/GatewaysList.tsx index d28728921df..834c1effef1 100644 --- a/ui/src/pages/apps/gateways/GatewaysList.tsx +++ b/ui/src/pages/apps/gateways/GatewaysList.tsx @@ -9,7 +9,6 @@ import { useToast } from "@/context/ToastContext"; import { queryKeys } from "@/lib/queryKeys"; import { toolsApi } from "@/api/tools"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -29,6 +28,7 @@ import { latestTokenActivity, } from "./gateway-helpers"; import { describeError } from "@/api/errors"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; export function GatewaysList() { const navigate = useNavigate(); @@ -73,8 +73,7 @@ export function GatewaysList() { enabled: !!selectedCompanyId, }); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId ?? "__none__"), - queryFn: () => projectsApi.list(selectedCompanyId!), + ...companyProjectsQuery(selectedCompanyId ?? "__none__"), enabled: !!selectedCompanyId, }); const gatewaysView = useQueryView(gatewaysQuery); diff --git a/ui/src/pages/skills/ImportSkillsFromProjectDialog.tsx b/ui/src/pages/skills/ImportSkillsFromProjectDialog.tsx index c77b9e31f5b..3d445ae6ca7 100644 --- a/ui/src/pages/skills/ImportSkillsFromProjectDialog.tsx +++ b/ui/src/pages/skills/ImportSkillsFromProjectDialog.tsx @@ -31,7 +31,6 @@ import { normalizeAgentUrlKey } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { ApiError } from "../../api/client"; import { companySkillsApi } from "../../api/companySkills"; -import { projectsApi } from "../../api/projects"; import { useToastActions } from "../../context/ToastContext"; import { queryKeys } from "../../lib/queryKeys"; import { skillStudioRoute } from "../../lib/company-skill-routes"; @@ -49,6 +48,7 @@ import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { EmptyState } from "../../components/EmptyState"; import { cn } from "../../lib/utils"; import { describeError } from "@/api/errors"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; type Step = "pick" | "scanning" | "select" | "result"; export type SkillSelection = { workspaceId: string; path: string; slug?: string }; @@ -295,8 +295,7 @@ export function ImportSkillsFromProjectDialog({ const scanTokenRef = useRef(0); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), + ...companyProjectsQuery(companyId), enabled: open, }); const projectsView = useQueryView(projectsQuery); diff --git a/ui/src/pages/tools/GatewaysTab.tsx b/ui/src/pages/tools/GatewaysTab.tsx index 2715426cb96..8478304b773 100644 --- a/ui/src/pages/tools/GatewaysTab.tsx +++ b/ui/src/pages/tools/GatewaysTab.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { type FormEvent, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { @@ -9,7 +10,6 @@ import type { } from "@paperclipai/shared"; import { Check, ChevronDown, Copy, KeyRound, Link as LinkIcon, Plus, RotateCcw, X } from "lucide-react"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { toolsApi } from "@/api/tools"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { Button } from "@/components/ui/button"; @@ -141,8 +141,7 @@ export function GatewaysTab({ companyId }: { companyId: string }) { queryFn: () => agentsApi.list(companyId), }); const projectsQuery = useQuery({ - queryKey: queryKeys.projects.list(companyId, { includeArchived: true }), - queryFn: () => projectsApi.list(companyId, { includeArchived: true }), + ...companyProjectsQuery(companyId, { includeArchived: true }), }); const gatewaysView = useQueryView(gatewaysQuery); diff --git a/ui/src/pages/tools/ProfilesTab.tsx b/ui/src/pages/tools/ProfilesTab.tsx index ded0ba0b9eb..f4bc4d9433b 100644 --- a/ui/src/pages/tools/ProfilesTab.tsx +++ b/ui/src/pages/tools/ProfilesTab.tsx @@ -1,3 +1,4 @@ +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { useMemo, useState } from "react"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Layers, Plus, Pencil, Trash2, Link2, ShieldCheck } from "lucide-react"; @@ -14,7 +15,6 @@ import type { ToolRiskLevel, } from "@paperclipai/shared"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { routinesApi } from "@/api/routines"; import { toolsApi, @@ -313,8 +313,7 @@ function useLookupData(companyId: string) { queryFn: () => agentsApi.list(companyId), }); const projects = useQuery({ - queryKey: queryKeys.projects.list(companyId, { includeArchived: true }), - queryFn: () => projectsApi.list(companyId, { includeArchived: true }), + ...companyProjectsQuery(companyId, { includeArchived: true }), }); const routines = useQuery({ queryKey: queryKeys.routines.list(companyId), diff --git a/ui/src/pages/tools/profiles/useProfilesData.ts b/ui/src/pages/tools/profiles/useProfilesData.ts index 1ac2a03d68d..6b201519370 100644 --- a/ui/src/pages/tools/profiles/useProfilesData.ts +++ b/ui/src/pages/tools/profiles/useProfilesData.ts @@ -1,11 +1,11 @@ import { useMemo } from "react"; import { useQueries, useQuery } from "@tanstack/react-query"; import { agentsApi } from "@/api/agents"; -import { projectsApi } from "@/api/projects"; import { routinesApi } from "@/api/routines"; import { toolsApi } from "@/api/tools"; import { queryKeys } from "@/lib/queryKeys"; import { groupCatalogByApp } from "./profile-model"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; /** * Shared data layer for the access-profiles index and wizard. Assembles the @@ -30,8 +30,7 @@ export function useProfilesData(companyId: string) { queryFn: () => agentsApi.list(companyId), }); const projects = useQuery({ - queryKey: queryKeys.projects.list(companyId), - queryFn: () => projectsApi.list(companyId), + ...companyProjectsQuery(companyId), }); const routines = useQuery({ queryKey: queryKeys.routines.list(companyId), diff --git a/ui/src/plugins/bridge-init.ts b/ui/src/plugins/bridge-init.ts index e94a3e880c4..9269db4c9db 100644 --- a/ui/src/plugins/bridge-init.ts +++ b/ui/src/plugins/bridge-init.ts @@ -21,6 +21,7 @@ import { usePluginStream, usePluginToast, } from "./bridge.js"; +import { companyProjectsQuery } from "@/lib/company-projects-query"; import { Component, createElement, useEffect, useMemo, useState, type ComponentType, type ReactNode } from "react"; import * as ReactJsxRuntimeModule from "react/jsx-runtime"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -39,7 +40,6 @@ import { agentsApi } from "@/api/agents"; import { authApi } from "@/api/auth"; import { heartbeatsApi } from "@/api/heartbeats"; import { issuesApi } from "@/api/issues"; -import { projectsApi } from "@/api/projects"; import { buildCompanyUserInlineOptions, } from "@/lib/company-members"; @@ -277,8 +277,7 @@ function PluginSdkIssuesList({ enabled: !!companyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(companyId ?? "__no-company__", { includeArchived: true }), - queryFn: () => projectsApi.list(companyId!, { includeArchived: true }), + ...companyProjectsQuery(companyId ?? "__no-company__", { includeArchived: true }), enabled: !!companyId, }); const liveRunsQueryKey = queryKeys.liveRuns(companyId ?? "__no-company__"); @@ -473,8 +472,7 @@ function PluginSdkProjectPicker({ }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(resolvedCompanyId ?? "__no-company__", { includeArchived }), - queryFn: () => projectsApi.list(resolvedCompanyId!, { includeArchived }), + ...companyProjectsQuery(resolvedCompanyId ?? "__no-company__", { includeArchived }), enabled: !!resolvedCompanyId, }); const visibleProjects = useMemo(