diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx index d4b263d4f01..665eea639dc 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx @@ -49,7 +49,7 @@ afterEach(async () => { vi.unstubAllGlobals() }) -async function render(canEditLogo = true) { +async function render(canEditLogo = true, isCollapsed = false, onExpandSidebar = vi.fn()) { await act(async () => { root.render( @@ -57,8 +57,8 @@ async function render(canEditLogo = true) { @@ -74,9 +74,9 @@ async function openMenu() { }) } -function menuItem(name: string) { - return Array.from(document.querySelectorAll('[role="menuitem"]')).find( - (item) => item.textContent === name +function logoControl() { + return document.querySelector( + '[role="menuitem"][aria-label="Change organization logo"]' ) } @@ -87,21 +87,70 @@ async function pickFile(file: File) { } describe('OrganizationHeader logo upload', () => { - it('opens the same native file picker from the admin menu', async () => { + it('opens the native file picker by clicking the logo and keeps the menu open', async () => { await render() await openMenu() const input = container.querySelector('input[type="file"]')! const click = vi.spyOn(input, 'click').mockImplementation(() => {}) expect(input.accept).toContain('image/png') - await act(async () => menuItem('Upload logo')!.click()) + await act(async () => logoControl()!.click()) + expect(click).toHaveBeenCalledOnce() + expect(logoControl()).not.toBeNull() + expect(document.body.textContent).not.toContain('Upload logo') + expect(document.querySelector('[role="menu"]')?.textContent).toContain('Settings') + }) + + it.each(['Enter', ' '])('opens the file picker using the %j key', async (key) => { + await render() + await openMenu() + const input = container.querySelector('input[type="file"]')! + const click = vi.spyOn(input, 'click').mockImplementation(() => {}) + await act(async () => { + logoControl()!.focus() + logoControl()!.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) + }) expect(click).toHaveBeenCalledOnce() }) it('does not offer logo changes to members', async () => { await render(false) await openMenu() - expect(menuItem('Upload logo')).toBeUndefined() + expect(logoControl()).toBeNull() + expect(container.querySelector('input[type="file"]')).toBeNull() + expect(document.querySelector('[role="menu"]')?.textContent).toContain('Design') + }) + + it('preserves the collapsed logo as the sidebar expand control', async () => { + const expand = vi.fn() + await render(true, true, expand) + await act(async () => { + container.querySelector('[aria-label="Expand sidebar"]')!.click() + }) + expect(expand).toHaveBeenCalledOnce() expect(container.querySelector('input[type="file"]')).toBeNull() + expect(mocks.upload).not.toHaveBeenCalled() + }) + + it('disables logo changes while the upload is pending', async () => { + let completeUpload!: () => void + mocks.upload.mockImplementation( + () => + new Promise((resolve) => { + completeUpload = resolve + }) + ) + await render() + await openMenu() + await pickFile(new File(['image'], 'logo.png', { type: 'image/png' })) + await act(async () => { + await vi.waitFor(() => expect(logoControl()?.getAttribute('aria-disabled')).toBe('true')) + }) + expect(logoControl()?.getAttribute('aria-busy')).toBe('true') + expect(container.querySelector('input[type="file"]')!.disabled).toBe(true) + await act(async () => logoControl()!.click()) + expect(mocks.upload).toHaveBeenCalledOnce() + await act(async () => completeUpload()) + expect(mocks.refresh).toHaveBeenCalledOnce() }) it('uploads under the organization scope and refreshes its identity after success', async () => { 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 de00a944798..19a580ae22b 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 @@ -9,9 +9,10 @@ import { DropdownMenuItem, DropdownMenuTrigger, OverflowText, + Tooltip, toast, } from '@sim/emcn' -import { PanelLeft, Settings, Upload } from '@sim/emcn/icons' +import { PanelLeft, Settings } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' import { IdentityTile } from '@/components/identity-tile/identity-tile' import { getOrganizationSettingsHref } from '@/components/settings/navigation' @@ -80,6 +81,9 @@ export function OrganizationHeader({ } const { memberCount } = organization + const logo = ( + + ) return (
@@ -120,12 +124,30 @@ export function OrganizationHeader({ className='w-64 max-w-[calc(100vw-24px)]' >
- + {canEditLogo ? ( + + + { + event.preventDefault() + fileInputRef.current?.click() + }} + > + {logo} + + + + {isUploadingLogo ? 'Uploading...' : 'Change logo'} + + + ) : ( + logo + )}
@@ -133,15 +155,6 @@ export function OrganizationHeader({
- {canEditLogo && ( - fileInputRef.current?.click()} - > - - {isUploadingLogo ? 'Uploading...' : 'Upload logo'} - - )}