Skip to content

Commit 18ef6bc

Browse files
committed
fix(organizations): let admins rename their organization
1 parent 86ad456 commit 18ef6bc

3 files changed

Lines changed: 102 additions & 9 deletions

File tree

‎apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx‎

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ import {
1313
toast,
1414
} from '@sim/emcn'
1515
import { PanelLeft, Send, Settings } from '@sim/emcn/icons'
16+
import { getErrorMessage } from '@sim/utils/errors'
1617
import { useRouter } from 'next/navigation'
1718
import { IdentityTile } from '@/components/identity-tile/identity-tile'
1819
import { getOrganizationSettingsHref } from '@/components/settings/navigation'
1920
import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link'
2021
import type { OrganizationSurfaceOrganization } from '@/lib/organizations/surface'
2122
import { LOGO_ACCEPT_ATTRIBUTE } from '@/lib/uploads/client/logo-file'
2223
import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal'
24+
import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row'
2325
import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
26+
import { useRenameOrganization } from '@/hooks/queries/organization'
2427
import { useUploadOrganizationLogo } from '@/hooks/queries/organization-logo'
2528

2629
function getOrganizationInitial(name: string): string {
@@ -29,7 +32,8 @@ function getOrganizationInitial(name: string): string {
2932

3033
interface OrganizationHeaderProps {
3134
organization: OrganizationSurfaceOrganization
32-
canEditLogo: boolean
35+
/** Organization admins may change its logo and name. */
36+
canEditIdentity: boolean
3337
canInviteMembers: boolean
3438
isCollapsed: boolean
3539
/** Expands the rail; the collapsed header is itself the expand control. */
@@ -42,10 +46,11 @@ interface OrganizationHeaderProps {
4246
* into its settings; collapsed, it becomes the rail's expand control, swapping
4347
* the mark for a panel glyph on hover exactly as the workspace header does. The
4448
* mark is the organization's uploaded logo or its initial on the neutral tile.
49+
* Admins change the logo and rename the organization from the card.
4550
*/
4651
export function OrganizationHeader({
4752
organization,
48-
canEditLogo,
53+
canEditIdentity,
4954
canInviteMembers,
5055
isCollapsed,
5156
onExpandSidebar,
@@ -55,7 +60,9 @@ export function OrganizationHeader({
5560
const { mutate: uploadLogo, isPending: isUploadingLogo } = useUploadOrganizationLogo(
5661
organization.id
5762
)
63+
const renameOrganization = useRenameOrganization()
5864
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false)
65+
const [draftName, setDraftName] = useState<string | null>(null)
5966
const initial = getOrganizationInitial(organization.name)
6067

6168
if (isCollapsed) {
@@ -85,13 +92,28 @@ export function OrganizationHeader({
8592
}
8693

8794
const { memberCount } = organization
95+
const stopRenaming = () => {
96+
setDraftName(null)
97+
renameOrganization.reset()
98+
}
99+
100+
const submitRename = () => {
101+
if (draftName === null || renameOrganization.isPending) return
102+
const name = draftName.trim()
103+
if (!name || name === organization.name) {
104+
stopRenaming()
105+
return
106+
}
107+
renameOrganization.mutate({ orgId: organization.id, name }, { onSuccess: stopRenaming })
108+
}
109+
88110
const logo = (
89111
<IdentityTile size='lg' initial={initial} logoUrl={organization.logo} alt={organization.name} />
90112
)
91113

92114
return (
93115
<div className='min-w-0 flex-1'>
94-
{canEditLogo && (
116+
{canEditIdentity && (
95117
<input
96118
ref={fileInputRef}
97119
type='file'
@@ -110,7 +132,11 @@ export function OrganizationHeader({
110132
}}
111133
/>
112134
)}
113-
<DropdownMenu>
135+
<DropdownMenu
136+
onOpenChange={(open) => {
137+
if (!open) stopRenaming()
138+
}}
139+
>
114140
<DropdownMenuTrigger asChild>
115141
<Chip
116142
aria-label='Organization menu'
@@ -128,7 +154,7 @@ export function OrganizationHeader({
128154
className='w-64 max-w-[calc(100vw-24px)]'
129155
>
130156
<div className='flex items-center gap-2 px-2 py-1.5'>
131-
{canEditLogo ? (
157+
{canEditIdentity ? (
132158
<Tooltip.Root>
133159
<Tooltip.Trigger asChild>
134160
<DropdownMenuItem
@@ -150,8 +176,50 @@ export function OrganizationHeader({
150176
) : (
151177
logo
152178
)}
153-
<div className='flex min-w-0 flex-col'>
154-
<OverflowText label={organization.name} />
179+
<div className='flex min-w-0 flex-1 flex-col'>
180+
{draftName !== null ? (
181+
<SidebarRenameRow
182+
aria-label='Organization name'
183+
aria-invalid={renameOrganization.isError || undefined}
184+
autoFocus
185+
value={draftName}
186+
disabled={renameOrganization.isPending}
187+
onFocus={(event) => event.currentTarget.select()}
188+
onChange={(event) => {
189+
setDraftName(event.target.value)
190+
if (renameOrganization.isError) renameOrganization.reset()
191+
}}
192+
onKeyDown={(event) => {
193+
if (event.key === 'Enter') {
194+
event.preventDefault()
195+
submitRename()
196+
} else if (event.key === 'Escape') {
197+
event.preventDefault()
198+
stopRenaming()
199+
}
200+
}}
201+
onBlur={submitRename}
202+
/>
203+
) : canEditIdentity ? (
204+
<DropdownMenuItem
205+
aria-label='Rename organization'
206+
textValue='Rename organization'
207+
className='h-auto min-w-0 p-0 hover-hover:opacity-70 focus-visible:opacity-70'
208+
onSelect={(event) => {
209+
event.preventDefault()
210+
setDraftName(organization.name)
211+
}}
212+
>
213+
<OverflowText label={organization.name} />
214+
</DropdownMenuItem>
215+
) : (
216+
<OverflowText label={organization.name} />
217+
)}
218+
{renameOrganization.isError && (
219+
<span role='alert' className='text-[var(--text-error)] text-caption'>
220+
{getErrorMessage(renameOrganization.error, 'Failed to rename organization')}
221+
</span>
222+
)}
155223
<span className='text-[var(--text-muted)] text-caption'>
156224
{memberCount} {memberCount === 1 ? 'member' : 'members'}
157225
</span>
@@ -176,7 +244,7 @@ export function OrganizationHeader({
176244
open={isInviteModalOpen}
177245
onOpenChange={setIsInviteModalOpen}
178246
organizationId={organization.id}
179-
isOrganizationAdmin={canEditLogo}
247+
isOrganizationAdmin={canEditIdentity}
180248
canInvite={canInviteMembers}
181249
/>
182250
)}

‎apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar({
201201
>
202202
<OrganizationHeader
203203
organization={organization}
204-
canEditLogo={viewer.isAdmin}
204+
canEditIdentity={viewer.isAdmin}
205205
canInviteMembers={viewer.canInviteMembers}
206206
isCollapsed={isCollapsed}
207207
onExpandSidebar={toggleCollapsed}

‎apps/sim/hooks/queries/organization.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
type RosterWorkspaceAccess,
2424
removeOrganizationMemberContract,
2525
transferOwnershipContract,
26+
updateOrganizationContract,
2627
updateOrganizationMemberRoleContract,
2728
updateOrganizationMemberUsageLimitContract,
2829
updateOrganizationUsageLimitContract,
@@ -167,6 +168,30 @@ export function useOrganization(orgId: string) {
167168
})
168169
}
169170

171+
interface RenameOrganizationParams {
172+
orgId: string
173+
name: string
174+
}
175+
176+
/**
177+
* Renames an organization. The organization surface reads its identity from the
178+
* server layout, so a successful rename also refreshes the route.
179+
*/
180+
export function useRenameOrganization() {
181+
const queryClient = useQueryClient()
182+
const router = useRouter()
183+
184+
return useMutation({
185+
mutationFn: ({ orgId, name }: RenameOrganizationParams) =>
186+
requestJson(updateOrganizationContract, { params: { id: orgId }, body: { name } }),
187+
onSuccess: () => router.refresh(),
188+
onSettled: (_data, _error, variables) => {
189+
queryClient.invalidateQueries({ queryKey: organizationKeys.detail(variables.orgId) })
190+
queryClient.invalidateQueries({ queryKey: organizationKeys.lists() })
191+
},
192+
})
193+
}
194+
170195
/**
171196
* Update organization usage limit mutation with optimistic updates
172197
*/

0 commit comments

Comments
 (0)