diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index abd21345589..cee9841d8e6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -18,3 +18,4 @@ export { useWorkflowOperations } from './use-workflow-operations' export { useWorkflowSelection } from './use-workflow-selection' export { useWorkspaceLogoUpload } from './use-workspace-logo-upload' export { useWorkspaceManagement } from './use-workspace-management' +export { WORKSPACE_LOGO_ACCEPT_ATTRIBUTE } from './workspace-logo-file' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts index a902e2f18bb..2cc1e3c0ead 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts @@ -2,10 +2,9 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { uploadInternalFileSession } from '@/lib/uploads/client/session-upload' +import { validateWorkspaceLogoFile } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file' const logger = createLogger('WorkspaceLogoUpload') -const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB -const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp'] interface UseWorkspaceLogoUploadProps { workspaceId?: string @@ -51,16 +50,6 @@ export function useWorkspaceLogoUpload({ setPreviewUrl(currentLogoUrl || null) }, [currentLogoUrl]) - const validateFile = useCallback((file: File): string | null => { - if (file.size > MAX_FILE_SIZE) { - return `File "${file.name}" is too large. Maximum size is 5MB.` - } - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { - return `File "${file.name}" is not a supported image format. Please use PNG, JPEG, SVG, or WebP.` - } - return null - }, []) - const uploadFileToServer = useCallback(async (file: File): Promise => { const targetWorkspaceId = workspaceIdRef.current if (!targetWorkspaceId) { @@ -78,7 +67,7 @@ export function useWorkspaceLogoUpload({ const processFile = useCallback( async (file: File) => { - const validationError = validateFile(file) + const validationError = validateWorkspaceLogoFile(file) if (validationError) { onErrorRef.current?.(validationError) return @@ -106,7 +95,7 @@ export function useWorkspaceLogoUpload({ setIsUploading(false) } }, - [uploadFileToServer, validateFile] + [uploadFileToServer] ) const handleFileChange = useCallback( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.test.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.test.ts new file mode 100644 index 00000000000..888fe0b2bac --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { + validateWorkspaceLogoFile, + WORKSPACE_LOGO_ACCEPT_ATTRIBUTE, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file' + +function file(overrides: Partial> = {}) { + return { + name: 'logo.png', + size: 1024, + type: 'image/png', + ...overrides, + } +} + +describe('workspace logo files', () => { + it('advertises and accepts GIF images', () => { + expect(WORKSPACE_LOGO_ACCEPT_ATTRIBUTE.split(',')).toContain('image/gif') + expect(validateWorkspaceLogoFile(file({ name: 'animated.gif', type: 'image/gif' }))).toBeNull() + }) + + it('rejects files larger than 5MB', () => { + expect(validateWorkspaceLogoFile(file({ size: 5 * 1024 * 1024 + 1 }))).toBe( + 'File "logo.png" is too large. Maximum size is 5MB.' + ) + }) + + it('lists GIF among the supported formats in validation errors', () => { + expect(validateWorkspaceLogoFile(file({ name: 'logo.bmp', type: 'image/bmp' }))).toBe( + 'File "logo.bmp" is not a supported image format. Please use PNG, JPEG, GIF, SVG, or WebP.' + ) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.ts new file mode 100644 index 00000000000..30169ed911c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/workspace-logo-file.ts @@ -0,0 +1,26 @@ +const MAX_WORKSPACE_LOGO_SIZE = 5 * 1024 * 1024 + +const WORKSPACE_LOGO_IMAGE_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/svg+xml', + 'image/webp', +] as const + +const WORKSPACE_LOGO_IMAGE_TYPE_SET = new Set(WORKSPACE_LOGO_IMAGE_TYPES) + +export const WORKSPACE_LOGO_ACCEPT_ATTRIBUTE = WORKSPACE_LOGO_IMAGE_TYPES.join(',') + +export function validateWorkspaceLogoFile( + file: Pick +): string | null { + if (file.size > MAX_WORKSPACE_LOGO_SIZE) { + return `File "${file.name}" is too large. Maximum size is 5MB.` + } + if (!WORKSPACE_LOGO_IMAGE_TYPE_SET.has(file.type)) { + return `File "${file.name}" is not a supported image format. Please use PNG, JPEG, GIF, SVG, or WebP.` + } + return null +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index d8c707f0e81..efe8b0c769e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -93,6 +93,7 @@ import { useWorkflowOperations, useWorkspaceLogoUpload, useWorkspaceManagement, + WORKSPACE_LOGO_ACCEPT_ATTRIBUTE, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { compareByOrder, @@ -1330,7 +1331,7 @@ export const Sidebar = memo(function Sidebar({