From 11fe4972b640016dc9f9556e2e153851fa808be2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 13 Apr 2026 15:28:48 +0100 Subject: [PATCH] fix(site): use readonly Organization[] and explicit is_default lookups (#24288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `OrganizationAutocomplete` declared `options: Organization[]` (mutable), but `useDashboard().organizations` returns `readonly Organization[]`. Callers forced to spread to satisfy the type even though the component never mutates the array. While fixing this, found two more issues with how organization context is resolved: - `AgentCreateForm`, `ChatPageContent`, and `OrganizationRedirect` used `organizations[0]` assuming the default org is first — nothing guarantees that ordering. - `ChatPageInput` was guessing the org from `useDashboard()` for file uploads on existing chats, even though the chat already has `organization_id`. In multi-org deployments, uploads could land in the wrong org. Changes: - Widen `OrganizationAutocomplete` `options` to `readonly Organization[]` - Remove unnecessary `[...organizations]` spread in `AgentCreateForm` - Replace all `organizations[0]` with `.find(o => o.is_default) ?? organizations[0]` for users not in the default org - Eliminate `[...organizations].sort()` allocation in `OrganizationRedirect` with direct `.find()` lookups (equivalent priority: editable default → any editable → viewable default → any viewable) - Thread `chat.organization_id` from `AgentChatPage` → `AgentChatPageView` → `ChatPageInput` so file uploads use the chat's actual org instead of guessing Closes #24285 > 🤖 PR initially created by Claude Opus 4.6 --- .../OrganizationAutocomplete.tsx | 2 +- site/src/pages/AgentsPage/AgentChatPage.tsx | 1 + .../AgentsPage/AgentChatPageView.stories.tsx | 1 + .../pages/AgentsPage/AgentChatPageView.tsx | 3 ++ .../AgentsPage/components/AgentCreateForm.tsx | 10 ++++--- .../AgentsPage/components/ChatPageContent.tsx | 6 ++-- .../OrganizationRedirect.tsx | 30 +++++++++---------- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx index f5c1b5a9d6..6dc7f19d29 100644 --- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx +++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx @@ -21,7 +21,7 @@ import { type OrganizationAutocompleteProps = { value: Organization | null; onChange: (organization: Organization | null) => void; - options: Organization[]; + options: readonly Organization[]; id?: string; required?: boolean; }; diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index 140599324c..f7945d9778 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -1233,6 +1233,7 @@ const AgentChatPage: FC = () => { return ( = ({ editing, ...overrides }) => { const props = { agentId: AGENT_ID, + organizationId: "test-org-id", chatTitle: "Help me refactor", persistedError: undefined as ChatDetailError | undefined, parentChat: undefined as TypesGen.Chat | undefined, diff --git a/site/src/pages/AgentsPage/AgentChatPageView.tsx b/site/src/pages/AgentsPage/AgentChatPageView.tsx index 6655c4d27c..478f208d00 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.tsx @@ -85,6 +85,7 @@ interface EditingState { interface AgentChatPageViewProps { // Chat data. agentId: string; + organizationId: string | undefined; chatTitle: string | undefined; parentChat: TypesGen.Chat | undefined; persistedError: ChatDetailError | undefined; @@ -176,6 +177,7 @@ interface AgentChatPageViewProps { export const AgentChatPageView: FC = ({ agentId, + organizationId, chatTitle, parentChat, persistedError, @@ -417,6 +419,7 @@ export const AgentChatPageView: FC = ({
= ({ modelOptions.some((modelOption) => modelOption.id === userSelectedModel) ? userSelectedModel : preferredModelID; + const initialOrg = + organizations.find((o) => o.is_default) ?? organizations[0]; const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( () => { const stored = localStorage.getItem(selectedWorkspaceIdStorageKey); @@ -207,8 +209,8 @@ export const AgentCreateForm: FC = ({ } if ( showOrganizations && - organizations[0] && - workspace.organization_id !== organizations[0].id + initialOrg && + workspace.organization_id !== initialOrg.id ) { localStorage.removeItem(selectedWorkspaceIdStorageKey); return null; @@ -217,7 +219,7 @@ export const AgentCreateForm: FC = ({ }, ); const [selectedOrg, setSelectedOrg] = useState( - organizations[0] ?? null, + initialOrg ?? null, ); const [pendingOrgChange, setPendingOrgChange] = useState(null); @@ -404,7 +406,7 @@ export const AgentCreateForm: FC = ({ id="organization" required value={selectedOrg} - options={[...organizations]} + options={organizations} onChange={(newOrg) => { const orgChanged = newOrg?.id !== selectedOrg?.id; if (orgChanged && attachments.length > 0) { diff --git a/site/src/pages/AgentsPage/components/ChatPageContent.tsx b/site/src/pages/AgentsPage/components/ChatPageContent.tsx index 8c5c071c31..9e4bea72a5 100644 --- a/site/src/pages/AgentsPage/components/ChatPageContent.tsx +++ b/site/src/pages/AgentsPage/components/ChatPageContent.tsx @@ -2,7 +2,6 @@ import { type FC, Profiler, type ReactNode, useEffect } from "react"; import { toast } from "sonner"; import type { UrlTransform } from "streamdown"; import type * as TypesGen from "#/api/typesGenerated"; -import { useDashboard } from "#/modules/dashboard/useDashboard"; import { useFileAttachments } from "../hooks/useFileAttachments"; import type { ChatDetailError } from "../utils/usageLimitMessage"; import { @@ -124,6 +123,8 @@ export type PendingAttachment = { }; interface ChatPageInputProps { + // Organization that owns this chat. Used to scope file uploads. + organizationId: string | undefined; store: ChatStoreHandle; compressionThreshold: number | undefined; onSend: ( @@ -181,6 +182,7 @@ interface ChatPageInputProps { } export const ChatPageInput: FC = ({ + organizationId, store, compressionThreshold, onSend, @@ -257,8 +259,6 @@ export const ChatPageInput: FC = ({ const latestContextUsage = rawUsage ? { ...rawUsage, compressionThreshold, lastInjectedContext } : rawUsage; - const { organizations } = useDashboard(); - const organizationId = organizations[0]?.id; const { attachments, textContents, diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx index 15ea630f65..996d8ce051 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx @@ -10,25 +10,25 @@ const OrganizationRedirect: FC = () => { organizationPermissionsByOrganizationId: organizationPermissions, } = useOrganizationSettings(); - const sortedOrganizations = [...organizations].sort( - (a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0), - ); - // Redirect /organizations => /organizations/some-organization-name - // If they can edit the default org, we should redirect to the default. - // If they cannot edit the default, we should redirect to the first org that - // they can edit. - const editableOrg = sortedOrganizations.find((org) => - canEditOrganization(organizationPermissions[org.id]), - ); + // Prefer the editable default org, then any editable org, then + // any viewable org. This replaces the previous [...organizations] + // .sort() approach with direct lookups to avoid copying the array. + const defaultOrg = organizations.find((org) => org.is_default); + const editableOrg = + (defaultOrg && + canEditOrganization(organizationPermissions[defaultOrg.id]) && + defaultOrg) || + organizations.find((org) => + canEditOrganization(organizationPermissions[org.id]), + ); if (editableOrg) { return ; } - // If they cannot edit any org, just redirect to an org they can read. - if (sortedOrganizations.length > 0) { - return ( - - ); + // If they cannot edit any org, just redirect to one they can read. + const viewableOrg = defaultOrg ?? organizations[0]; + if (viewableOrg) { + return ; } return ; };