From 18ef6bc13a95a6b5ed1381513e6b97d120f12b14 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 2 Nov 2026 18:43:38 -0800 Subject: [PATCH 1/2] fix(organizations): let admins rename their organization --- .../organization-header.tsx | 84 +++++++++++++++++-- .../organization-sidebar.tsx | 2 +- apps/sim/hooks/queries/organization.ts | 25 ++++++ 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx index a5790cd9e35..d813600f3c3 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx @@ -13,6 +13,7 @@ import { toast, } from '@sim/emcn' import { PanelLeft, Send, Settings } from '@sim/emcn/icons' +import { getErrorMessage } from '@sim/utils/errors' import { useRouter } from 'next/navigation' import { IdentityTile } from '@/components/identity-tile/identity-tile' import { getOrganizationSettingsHref } from '@/components/settings/navigation' @@ -20,7 +21,9 @@ import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link import type { OrganizationSurfaceOrganization } from '@/lib/organizations/surface' import { LOGO_ACCEPT_ATTRIBUTE } from '@/lib/uploads/client/logo-file' import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal' +import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row' import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' +import { useRenameOrganization } from '@/hooks/queries/organization' import { useUploadOrganizationLogo } from '@/hooks/queries/organization-logo' function getOrganizationInitial(name: string): string { @@ -29,7 +32,8 @@ function getOrganizationInitial(name: string): string { interface OrganizationHeaderProps { organization: OrganizationSurfaceOrganization - canEditLogo: boolean + /** Organization admins may change its logo and name. */ + canEditIdentity: boolean canInviteMembers: boolean isCollapsed: boolean /** Expands the rail; the collapsed header is itself the expand control. */ @@ -42,10 +46,11 @@ interface OrganizationHeaderProps { * into its settings; collapsed, it becomes the rail's expand control, swapping * the mark for a panel glyph on hover exactly as the workspace header does. The * mark is the organization's uploaded logo or its initial on the neutral tile. + * Admins change the logo and rename the organization from the card. */ export function OrganizationHeader({ organization, - canEditLogo, + canEditIdentity, canInviteMembers, isCollapsed, onExpandSidebar, @@ -55,7 +60,9 @@ export function OrganizationHeader({ const { mutate: uploadLogo, isPending: isUploadingLogo } = useUploadOrganizationLogo( organization.id ) + const renameOrganization = useRenameOrganization() const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + const [draftName, setDraftName] = useState(null) const initial = getOrganizationInitial(organization.name) if (isCollapsed) { @@ -85,13 +92,28 @@ export function OrganizationHeader({ } const { memberCount } = organization + const stopRenaming = () => { + setDraftName(null) + renameOrganization.reset() + } + + const submitRename = () => { + if (draftName === null || renameOrganization.isPending) return + const name = draftName.trim() + if (!name || name === organization.name) { + stopRenaming() + return + } + renameOrganization.mutate({ orgId: organization.id, name }, { onSuccess: stopRenaming }) + } + const logo = ( ) return (
- {canEditLogo && ( + {canEditIdentity && ( )} - + { + if (!open) stopRenaming() + }} + >
- {canEditLogo ? ( + {canEditIdentity ? ( - +
+ {draftName !== null ? ( + event.currentTarget.select()} + onChange={(event) => { + setDraftName(event.target.value) + if (renameOrganization.isError) renameOrganization.reset() + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault() + submitRename() + } else if (event.key === 'Escape') { + event.preventDefault() + stopRenaming() + } + }} + onBlur={submitRename} + /> + ) : canEditIdentity ? ( + { + event.preventDefault() + setDraftName(organization.name) + }} + > + + + ) : ( + + )} + {renameOrganization.isError && ( + + {getErrorMessage(renameOrganization.error, 'Failed to rename organization')} + + )} {memberCount} {memberCount === 1 ? 'member' : 'members'} @@ -176,7 +244,7 @@ export function OrganizationHeader({ open={isInviteModalOpen} onOpenChange={setIsInviteModalOpen} organizationId={organization.id} - isOrganizationAdmin={canEditLogo} + isOrganizationAdmin={canEditIdentity} canInvite={canInviteMembers} /> )} diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx index e465ddbc458..5d7d19dc225 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx @@ -201,7 +201,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar({ > + requestJson(updateOrganizationContract, { params: { id: orgId }, body: { name } }), + onSuccess: () => router.refresh(), + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: organizationKeys.detail(variables.orgId) }) + queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }) + }, + }) +} + /** * Update organization usage limit mutation with optimistic updates */ From 3eb056cbeab85534f360f739dc4c340eb24dc1a6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 2 Nov 2026 18:59:44 -0800 Subject: [PATCH 2/2] fix(organizations): keep the rename draft until it is saved or cancelled --- .../organization-header.tsx | 19 +++++++++++++++++-- .../collapsed-sidebar-menu.tsx | 14 ++++---------- .../w/components/sidebar/utils.ts | 11 ++++++++++- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx index d813600f3c3..c0e4946dd0c 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx @@ -23,6 +23,7 @@ import { LOGO_ACCEPT_ATTRIBUTE } from '@/lib/uploads/client/logo-file' import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal' import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row' import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' +import { holdRowFocus } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useRenameOrganization } from '@/hooks/queries/organization' import { useUploadOrganizationLogo } from '@/hooks/queries/organization-logo' @@ -56,12 +57,14 @@ export function OrganizationHeader({ onExpandSidebar, }: OrganizationHeaderProps) { const fileInputRef = useRef(null) + const nameInputRef = useRef(null) const router = useRouter() const { mutate: uploadLogo, isPending: isUploadingLogo } = useUploadOrganizationLogo( organization.id ) const renameOrganization = useRenameOrganization() const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + const [isMenuOpen, setIsMenuOpen] = useState(false) const [draftName, setDraftName] = useState(null) const initial = getOrganizationInitial(organization.name) @@ -104,7 +107,13 @@ export function OrganizationHeader({ stopRenaming() return } - renameOrganization.mutate({ orgId: organization.id, name }, { onSuccess: stopRenaming }) + renameOrganization.mutate( + { orgId: organization.id, name }, + { + onSuccess: stopRenaming, + onError: () => requestAnimationFrame(() => nameInputRef.current?.focus()), + } + ) } const logo = ( @@ -133,8 +142,11 @@ export function OrganizationHeader({ /> )} { - if (!open) stopRenaming() + // The input's blur saves the draft; closing now would unmount it and hide a failure. + if (!open && draftName !== null) return + setIsMenuOpen(open) }} > @@ -152,6 +164,8 @@ export function OrganizationHeader({ side='bottom' sideOffset={8} className='w-64 max-w-[calc(100vw-24px)]' + onPointerMoveCapture={draftName !== null ? holdRowFocus : undefined} + onPointerOutCapture={draftName !== null ? holdRowFocus : undefined} >
{canEditIdentity ? ( @@ -179,6 +193,7 @@ export function OrganizationHeader({
{draftName !== null ? ( { - if (e.pointerType === 'mouse') e.preventDefault() -} - const EDIT_ROW_CLASS = cn( chipVariants({ active: true, fullWidth: true }), 'min-w-0 cursor-default select-none text-small' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts index 62f818b88bb..a6ed4725eb6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts @@ -1,10 +1,19 @@ -import type { MouseEvent } from 'react' +import type { MouseEvent, PointerEvent } from 'react' import type { MothershipResource } from '@/lib/mothership/resource-types' import { getFolderMap } from '@/hooks/queries/utils/folder-cache' import { getWorkflows } from '@/hooks/queries/utils/workflow-cache' import type { FolderTreeNode } from '@/stores/folders/types' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +/** + * Prevents Radix's mouse pointer handlers from transferring focus. Row handlers keep + * actions submenus open; content capture handlers preserve inline rename focus, + * including inside portaled folders. Radix skips handlers for defaulted events. + */ +export function holdRowFocus(event: PointerEvent) { + if (event.pointerType === 'mouse') event.preventDefault() +} + /** Distinguishes sidebar background clicks from controls and portaled overlays. */ export function isSidebarBackgroundClick(event: MouseEvent): boolean { return (