From e51bc5715aa391938f2113f7bf2a113b4eec009c Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 13 Jun 2026 10:59:00 -0700 Subject: [PATCH] fix(skills): reuse shared upload field in skill import modal; logo-only Quartr icon (#5026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(skills): reuse shared upload field in skill import modal; logo-only Quartr icon - Replace the hand-rolled drop zone in the skill import modal with the shared ChipModalField type='file' control (same component the Knowledge Base and Help & Support modals use), so the upload zone is visually consistent. - Migrate the GitHub-URL and paste-content rows to ChipModalField so every field shares the canonical px-4 gutter and error rendering, and align the 'or' dividers to match. - Drop the monospace font on the paste textarea so its text matches the rest of the modal. - Quartr icon now renders the logo mark only (no wordmark) as a black mark on a white rounded tile. * fix(emcn): restore upload spinner via loading prop on ChipModalField file control Addresses review feedback — the shared file drop zone now accepts an optional loading prop that renders an animated spinner and blocks further picks while an async import is in flight, restoring the feedback the skill import modal lost when it migrated off its bespoke drop zone. --- .../components/skill-import/skill-import.tsx | 158 +++++------------- .../emcn/components/chip-modal/chip-modal.tsx | 21 ++- apps/sim/components/icons.tsx | 55 ++---- 3 files changed, 78 insertions(+), 156 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx index 968b63b381..7b2323c13a 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx @@ -1,13 +1,11 @@ 'use client' import type { ChangeEvent } from 'react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { getErrorMessage } from '@sim/utils/errors' -import { Chip, ChipInput, ChipTextarea, Loader } from '@/components/emcn' -import { Upload } from '@/components/emcn/icons' +import { Chip, ChipInput, ChipModalField, ChipTextarea, Loader } from '@/components/emcn' import { requestJson } from '@/lib/api/client/request' import { importSkillContract } from '@/lib/api/contracts' -import { cn } from '@/lib/core/utils/cn' import { extractSkillFromZip, parseSkillMarkdown, @@ -33,10 +31,6 @@ function isAcceptedFile(file: File): boolean { } export function SkillImport({ onImport }: SkillImportProps) { - const fileInputRef = useRef(null) - - const [dragCounter, setDragCounter] = useState(0) - const isDragging = dragCounter > 0 const [fileState, setFileState] = useState('idle') const [fileError, setFileError] = useState('') @@ -84,39 +78,9 @@ export function SkillImport({ onImport }: SkillImportProps) { [onImport] ) - const handleFileChange = useCallback( - (e: ChangeEvent) => { - const file = e.target.files?.[0] - if (file) processFile(file) - if (fileInputRef.current) fileInputRef.current.value = '' - }, - [processFile] - ) - - const handleDragEnter = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => prev + 1) - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => prev - 1) - }, []) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - e.dataTransfer.dropEffect = 'copy' - }, []) - - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter(0) - const file = e.dataTransfer.files?.[0] + const handleFiles = useCallback( + (files: File[]) => { + const file = files[0] if (file) processFile(file) }, [processFile] @@ -159,55 +123,20 @@ export function SkillImport({ onImport }: SkillImportProps) { return (
- {/* File drop zone */} -
- Upload File - - {fileError &&

{fileError}

} -
+ - {/* GitHub URL */} -
- - Import from GitHub - +
: 'Fetch'}
- {githubError &&

{githubError}

} -
+
- {/* Paste content */} -
- - Paste SKILL.md Content - - ) => { - setPasteContent(e.target.value) - if (pasteError) setPasteError('') - }} - resizable - className='min-h-[120px] font-mono leading-relaxed' - /> - {pasteError &&

{pasteError}

} -
- - Import - + +
+ ) => { + setPasteContent(e.target.value) + if (pasteError) setPasteError('') + }} + resizable + className='min-h-[120px]' + /> +
+ + Import + +
-
+
) } function ImportDivider() { return ( -
+
or
diff --git a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx index d2079583bb..b15f2031ad 100644 --- a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx +++ b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx @@ -52,6 +52,7 @@ import { ChipTextarea } from '@/components/emcn/components/chip-textarea/chip-te import { Label } from '@/components/emcn/components/label/label' import { Modal, ModalContent } from '@/components/emcn/components/modal/modal' import { TagInput, type TagItem } from '@/components/emcn/components/tag-input/tag-input' +import { Loader } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' @@ -377,6 +378,14 @@ interface ChipModalFileFieldProps extends ChipModalFieldBaseProps { * for a single-line zone. */ description?: React.ReactNode + /** + * Renders a spinner inside the drop zone and blocks further picks while an + * async import/upload is in flight. Use for slow selections (zip extraction, + * remote fetches) where the zone would otherwise look idle. Pair with a + * `label` such as `'Importing…'` for an explicit status line. + * @default false + */ + loading?: boolean } export interface ChipModalEmailsFieldProps extends ChipModalFieldBaseProps { @@ -692,6 +701,7 @@ function ChipModalFileControl({ multiple = false, label = 'Drop files here or click to browse', description, + loading = false, disabled, id, 'aria-required': ariaRequired, @@ -700,6 +710,7 @@ function ChipModalFileControl({ }: ChipModalFileFieldProps & { id: string } & React.AriaAttributes) { const inputRef = React.useRef(null) const [isDragging, setIsDragging] = React.useState(false) + const isInteractive = !disabled && !loading const emitFiles = React.useCallback( (files: FileList | null) => { @@ -713,7 +724,8 @@ function ChipModalFileControl({