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 new file mode 100644 index 00000000000..2bbe3fc2288 --- /dev/null +++ b/apps/sim/components/icons/document-icons.test.ts @@ -0,0 +1,21 @@ +/** @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) + expect(getDocumentIcon('application/x-zip', '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 cd4436318d6..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' @@ -218,6 +220,28 @@ export function MarkdownIcon(props: SVGProps) { ) } +export function ZipIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function DefaultFileIcon(props: SVGProps) { return ( ) { } 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 ( @@ -297,6 +322,16 @@ export function getDocumentIcon( return PptxIcon } + if ( + SUPPORTED_ARCHIVE_MIME_TYPES.includes(mimeType) || + (extension && + SUPPORTED_ARCHIVE_EXTENSIONS.includes( + extension as (typeof SUPPORTED_ARCHIVE_EXTENSIONS)[number] + )) + ) { + return ZipIcon + } + if (mimeType === 'text/x-sim-chart' || extension === 'chart') { return ChartFileIcon } 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',