From ea9d2ce1bd8e3fc8c4ad9a8bdd6433c95842d62f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 14 Sep 2026 13:51:55 -0700 Subject: [PATCH 1/3] feat(files): add zip file-type icon --- .../components/icons/document-icons.test.ts | 15 ++++++++++ apps/sim/components/icons/document-icons.tsx | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 apps/sim/components/icons/document-icons.test.ts diff --git a/apps/sim/components/icons/document-icons.test.ts b/apps/sim/components/icons/document-icons.test.ts new file mode 100644 index 00000000000..00a1fafe40d --- /dev/null +++ b/apps/sim/components/icons/document-icons.test.ts @@ -0,0 +1,15 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { DefaultFileIcon, getDocumentIcon, ZipIcon } from '@/components/icons/document-icons' + +describe('getDocumentIcon', () => { + it('uses the zip icon for zip archives by extension or mime type', () => { + expect(getDocumentIcon('', 'Package.ZIP')).toBe(ZipIcon) + expect(getDocumentIcon('application/zip', 'archive')).toBe(ZipIcon) + expect(getDocumentIcon('application/x-zip-compressed', 'archive')).toBe(ZipIcon) + }) + + it('falls back to the default icon for unknown types', () => { + expect(getDocumentIcon('application/octet-stream', 'blob.bin')).toBe(DefaultFileIcon) + }) +}) diff --git a/apps/sim/components/icons/document-icons.tsx b/apps/sim/components/icons/document-icons.tsx index cd4436318d6..e61c92d7c4b 100644 --- a/apps/sim/components/icons/document-icons.tsx +++ b/apps/sim/components/icons/document-icons.tsx @@ -218,6 +218,28 @@ export function MarkdownIcon(props: SVGProps) { ) } +export function ZipIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function DefaultFileIcon(props: SVGProps) { return ( Date: Mon, 14 Sep 2026 13:56:49 -0700 Subject: [PATCH 2/3] fix(files): normalize mime type before icon matching --- apps/sim/components/icons/document-icons.test.ts | 5 +++++ apps/sim/components/icons/document-icons.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/sim/components/icons/document-icons.test.ts b/apps/sim/components/icons/document-icons.test.ts index 00a1fafe40d..cc410977dc5 100644 --- a/apps/sim/components/icons/document-icons.test.ts +++ b/apps/sim/components/icons/document-icons.test.ts @@ -9,6 +9,11 @@ describe('getDocumentIcon', () => { expect(getDocumentIcon('application/x-zip-compressed', 'archive')).toBe(ZipIcon) }) + it('ignores mime type casing and parameters', () => { + expect(getDocumentIcon('APPLICATION/ZIP', 'archive')).toBe(ZipIcon) + expect(getDocumentIcon('application/zip; charset=binary', 'archive')).toBe(ZipIcon) + }) + it('falls back to the default icon for unknown types', () => { expect(getDocumentIcon('application/octet-stream', 'blob.bin')).toBe(DefaultFileIcon) }) diff --git a/apps/sim/components/icons/document-icons.tsx b/apps/sim/components/icons/document-icons.tsx index e61c92d7c4b..097cc110193 100644 --- a/apps/sim/components/icons/document-icons.tsx +++ b/apps/sim/components/icons/document-icons.tsx @@ -259,9 +259,10 @@ export function DefaultFileIcon(props: SVGProps) { } export function getDocumentIcon( - mimeType: string, + rawMimeType: string, filename: string ): (props: SVGProps) => React.JSX.Element { + const mimeType = rawMimeType.split(';')[0].trim().toLowerCase() const extension = filename.split('.').pop()?.toLowerCase() if ( From 518fc1441a82b712015f0c3ab5b3907ded3d36bb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 14 Sep 2026 14:00:20 -0700 Subject: [PATCH 3/3] improvement(files): make shared document icons the single source of truth --- .../components/shared/zip-icon/index.ts | 1 - .../components/shared/zip-icon/zip-icon.tsx | 28 -- .../files/components/files-hero-loop.tsx | 3 +- .../knowledge/[id]/[documentId]/document.tsx | 2 +- .../[workspaceId]/knowledge/[id]/base.tsx | 2 +- .../base-tags-modal/base-tags-modal.tsx | 2 +- .../components/icons/document-icons.tsx | 293 ------------------ .../knowledge/components/icons/index.ts | 15 - .../knowledge/components/index.ts | 1 - .../components/icons/document-icons.test.ts | 1 + apps/sim/components/icons/document-icons.tsx | 10 +- apps/sim/lib/uploads/utils/validation.ts | 2 +- 12 files changed, 13 insertions(+), 347 deletions(-) delete mode 100644 apps/sim/app/(landing)/components/shared/zip-icon/index.ts delete mode 100644 apps/sim/app/(landing)/components/shared/zip-icon/zip-icon.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/index.ts diff --git a/apps/sim/app/(landing)/components/shared/zip-icon/index.ts b/apps/sim/app/(landing)/components/shared/zip-icon/index.ts deleted file mode 100644 index b7cefcad470..00000000000 --- a/apps/sim/app/(landing)/components/shared/zip-icon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ZipIcon } from './zip-icon' diff --git a/apps/sim/app/(landing)/components/shared/zip-icon/zip-icon.tsx b/apps/sim/app/(landing)/components/shared/zip-icon/zip-icon.tsx deleted file mode 100644 index 3ae18082677..00000000000 --- a/apps/sim/app/(landing)/components/shared/zip-icon/zip-icon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { SVGProps } from 'react' - -/** - * Generic zip-archive glyph shared by the landing files surfaces (the - * homepage files preview and the files hero loop) - no dedicated zip - * component exists in the icon set. Inherits `currentColor` and sizes via - * className, matching the icon-set convention. - */ -export function ZipIcon(props: SVGProps) { - return ( - - - - - - - - - ) -} diff --git a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx index 2d84f21429b..a787c1d7ea9 100644 --- a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx +++ b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx @@ -5,10 +5,9 @@ import { useState } from 'react' import { cn } from '@sim/emcn' import { ArrowUpDown, File, ListFilter, Plus, Search } from '@sim/emcn/icons' import { AgentIcon } from '@/components/icons' -import { CsvIcon, DocxIcon, PdfIcon } from '@/components/icons/document-icons' +import { CsvIcon, DocxIcon, PdfIcon, ZipIcon } from '@/components/icons/document-icons' import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell' import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants' -import { ZipIcon } from '@/app/(landing)/components/shared/zip-icon' import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle' /** Sidebar content for the files hero - a file-heavy team's workspace. */ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index c8e621a49ec..eb262824d69 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -20,6 +20,7 @@ import { truncate } from '@sim/utils/string' import { useParams, useRouter } from 'next/navigation' import { useQueryStates } from 'nuqs' import { EmptyState } from '@/components/empty-state/empty-state' +import { getDocumentIcon } from '@/components/icons/document-icons' import { getDocumentIndexingStatus } from '@/lib/knowledge/documents/types' import type { ChunkData } from '@/lib/knowledge/types' import { formatTokenCount } from '@/lib/tokenization' @@ -58,7 +59,6 @@ import { documentUrlKeys, } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/search-params' import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar' -import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' import { useDocument, useDocumentChunks, useKnowledgeBase } from '@/hooks/kb/use-knowledge' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 61693dfadd0..9330027ea58 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -41,6 +41,7 @@ import { format } from 'date-fns' import { useParams, useRouter } from 'next/navigation' import { useQueryState, useQueryStates } from 'nuqs' import { usePostHog } from 'posthog-js/react' +import { getDocumentIcon } from '@/components/icons/document-icons' import { ALL_TAG_SLOTS, type AllTagSlot, @@ -104,7 +105,6 @@ import { documentFiltersUrlKeys, kbDocumentSortParams, } from '@/app/workspace/[workspaceId]/knowledge/[id]/search-params' -import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' import { canDeleteKnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/permissions' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx index aa2d370e492..f005dfb62b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx @@ -16,6 +16,7 @@ import { } from '@sim/emcn' import { Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { getDocumentIcon } from '@/components/icons/document-icons' import type { TagUsageData } from '@/lib/api/contracts/knowledge' import { FIELD_TYPE_LABELS, @@ -23,7 +24,6 @@ import { SUPPORTED_FIELD_TYPES, TAG_SLOT_CONFIG, } from '@/lib/knowledge/constants' -import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' import { type TagDefinition, useKnowledgeBaseTagDefinitions, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx deleted file mode 100644 index 6b669a44d6f..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/document-icons.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import type { SVGProps } from 'react' -import { - SUPPORTED_AUDIO_EXTENSIONS, - SUPPORTED_VIDEO_EXTENSIONS, -} from '@/lib/uploads/utils/validation' - -export function PdfIcon(props: SVGProps) { - return ( - - - - PDF - - - ) -} - -export function DocxIcon(props: SVGProps) { - return ( - - - - - - - - ) -} - -export function XlsxIcon(props: SVGProps) { - return ( - - - - - - - - ) -} - -export function CsvIcon(props: SVGProps) { - return ( - - - - - - - - - ) -} - -export function TxtIcon(props: SVGProps) { - return ( - - - - - - - ) -} - -export function PptxIcon(props: SVGProps) { - return ( - - - - - - ) -} - -export function AudioIcon(props: SVGProps) { - return ( - - - - - - - - ) -} - -export function VideoIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function HtmlIcon(props: SVGProps) { - return ( - - - - - - ) -} - -export function JsonIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function MarkdownIcon(props: SVGProps) { - return ( - - - - - - ) -} - -export function DefaultFileIcon(props: SVGProps) { - return ( - - - - - ) -} - -export function getDocumentIcon( - mimeType: string, - filename: string -): (props: SVGProps) => React.JSX.Element { - const extension = filename.split('.').pop()?.toLowerCase() - - if ( - mimeType.startsWith('audio/') || - (extension && - SUPPORTED_AUDIO_EXTENSIONS.includes(extension as (typeof SUPPORTED_AUDIO_EXTENSIONS)[number])) - ) { - return AudioIcon - } - - if ( - mimeType.startsWith('video/') || - (extension && - SUPPORTED_VIDEO_EXTENSIONS.includes(extension as (typeof SUPPORTED_VIDEO_EXTENSIONS)[number])) - ) { - return VideoIcon - } - - if (mimeType === 'application/pdf' || extension === 'pdf') { - return PdfIcon - } - - if ( - mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || - mimeType === 'application/msword' || - extension === 'docx' || - extension === 'doc' - ) { - return DocxIcon - } - - if ( - mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || - mimeType === 'application/vnd.ms-excel' || - extension === 'xlsx' || - extension === 'xls' - ) { - return XlsxIcon - } - - if (mimeType === 'text/csv' || extension === 'csv') { - return CsvIcon - } - - if (mimeType === 'text/plain' || extension === 'txt') { - return TxtIcon - } - - if ( - mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || - mimeType === 'application/vnd.ms-powerpoint' || - extension === 'pptx' || - extension === 'ppt' - ) { - return PptxIcon - } - - if (mimeType === 'text/html' || extension === 'html' || extension === 'htm') { - return HtmlIcon - } - - if (mimeType === 'application/json' || extension === 'json') { - return JsonIcon - } - - if (mimeType === 'text/markdown' || extension === 'md' || extension === 'mdx') { - return MarkdownIcon - } - - return DefaultFileIcon -} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/index.ts deleted file mode 100644 index 3ee65893fdb..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/icons/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { - AudioIcon, - CsvIcon, - DefaultFileIcon, - DocxIcon, - getDocumentIcon, - HtmlIcon, - JsonIcon, - MarkdownIcon, - PdfIcon, - PptxIcon, - TxtIcon, - VideoIcon, - XlsxIcon, -} from './document-icons' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts index 7fa05975a72..f8a396f34b0 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts @@ -1,6 +1,5 @@ export { CreateBaseModal } from './create-base-modal' export { DeleteKnowledgeBaseModal } from './delete-knowledge-base-modal' export { EditKnowledgeBaseModal } from './edit-knowledge-base-modal' -export { getDocumentIcon } from './icons' export { KnowledgeBaseContextMenu } from './knowledge-base-context-menu' export { KnowledgeListContextMenu } from './knowledge-list-context-menu' diff --git a/apps/sim/components/icons/document-icons.test.ts b/apps/sim/components/icons/document-icons.test.ts index cc410977dc5..2bbe3fc2288 100644 --- a/apps/sim/components/icons/document-icons.test.ts +++ b/apps/sim/components/icons/document-icons.test.ts @@ -7,6 +7,7 @@ describe('getDocumentIcon', () => { expect(getDocumentIcon('', 'Package.ZIP')).toBe(ZipIcon) expect(getDocumentIcon('application/zip', 'archive')).toBe(ZipIcon) expect(getDocumentIcon('application/x-zip-compressed', 'archive')).toBe(ZipIcon) + expect(getDocumentIcon('application/x-zip', 'archive')).toBe(ZipIcon) }) it('ignores mime type casing and parameters', () => { diff --git a/apps/sim/components/icons/document-icons.tsx b/apps/sim/components/icons/document-icons.tsx index 097cc110193..a35f97ba255 100644 --- a/apps/sim/components/icons/document-icons.tsx +++ b/apps/sim/components/icons/document-icons.tsx @@ -1,5 +1,7 @@ import type { SVGProps } from 'react' import { + SUPPORTED_ARCHIVE_EXTENSIONS, + SUPPORTED_ARCHIVE_MIME_TYPES, SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_VIDEO_EXTENSIONS, } from '@/lib/uploads/utils/validation' @@ -321,9 +323,11 @@ export function getDocumentIcon( } if ( - mimeType === 'application/zip' || - mimeType === 'application/x-zip-compressed' || - extension === 'zip' + SUPPORTED_ARCHIVE_MIME_TYPES.includes(mimeType) || + (extension && + SUPPORTED_ARCHIVE_EXTENSIONS.includes( + extension as (typeof SUPPORTED_ARCHIVE_EXTENSIONS)[number] + )) ) { return ZipIcon } diff --git a/apps/sim/lib/uploads/utils/validation.ts b/apps/sim/lib/uploads/utils/validation.ts index 72680c87f41..86ec94fdf0b 100644 --- a/apps/sim/lib/uploads/utils/validation.ts +++ b/apps/sim/lib/uploads/utils/validation.ts @@ -205,7 +205,7 @@ const SUPPORTED_IMAGE_MIME_TYPES = [ 'image/vnd.microsoft.icon', ] -const SUPPORTED_ARCHIVE_MIME_TYPES = [ +export const SUPPORTED_ARCHIVE_MIME_TYPES = [ 'application/zip', 'application/x-zip-compressed', 'application/x-zip',