diff --git a/.changeset/fn-7304-quick-add-attachments.md b/.changeset/fn-7304-quick-add-attachments.md new file mode 100644 index 0000000000..ff394affe7 --- /dev/null +++ b/.changeset/fn-7304-quick-add-attachments.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add icon-only Quick Add image attachments with drag-and-drop support. +category: feature +dev: QuickEntryBox now shares image selection, paste, and drop intake with accessible pending-count labels. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index e401fead2c..a73386a6e0 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -355,6 +355,9 @@ The **New Task** dialog's workflow selector also defaults to the current or last Optional workflow steps declared by the active workflow are available from the quick-add action row and the **New Task** dialog's inline quick buttons. For example, the coding workflow's browser verification option appears as a quick drop-down when that workflow is active; each option is seeded from the workflow step's `defaultOn` setting and is sent with the task's `enabledWorkflowSteps` payload at creation time. + +Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. + Quick entry, inline quick-create, and the full **New Task** dialog all check for similar active tasks before creating. When possible duplicates exist, the warning lists each match by task description (falling back to title, then “No description”) and lets you open an existing task, cancel, or create anyway with the duplicates acknowledged. Completed single-task planning sessions remain in the Planning Mode history after you create the task, and selecting one restores the completed summary instead of restarting the composer. History rows are deduplicated by session id even if the initial load and live session updates arrive out of order, and deleting a history entry now waits for the server delete to persist (failures keep the row visible and surface an error instead of silently disappearing until refresh). diff --git a/packages/dashboard/app/components/QuickEntryBox.css b/packages/dashboard/app/components/QuickEntryBox.css index a5a93b1bf3..11956aefbd 100644 --- a/packages/dashboard/app/components/QuickEntryBox.css +++ b/packages/dashboard/app/components/QuickEntryBox.css @@ -1,5 +1,6 @@ /* === Quick Entry Box === */ .quick-entry-box { + position: relative; padding: 8px 10px; background: var(--card); border: 1px solid var(--border); @@ -11,6 +12,54 @@ border-color: var(--todo); } +.quick-entry-box--drag-over { + border-color: var(--triage); + box-shadow: 0 0 0 var(--space-2xs) color-mix(in srgb, var(--triage) 35%, transparent); +} + +.quick-entry-drop-target { + position: absolute; + inset: var(--space-xs); + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + padding: var(--space-sm); + border: var(--space-2xs) dashed var(--triage); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--card) 86%, var(--triage)); + color: var(--text); + font-size: 0.8125rem; + font-weight: 600; + pointer-events: none; +} + +.quick-entry-attach-button { + position: relative; + min-width: calc(var(--space-xl) + var(--space-xs)); + gap: var(--space-2xs); +} + +.quick-entry-attach-count { + min-width: var(--space-md); + padding: 0 var(--space-2xs); + border-radius: calc(var(--radius) * 4); + background: var(--triage); + color: var(--provider-icon-contrast); + font-size: 0.6875rem; + line-height: 1.2; + font-weight: 700; +} + +@media (max-width: 768px) { + .quick-entry-drop-target { + inset: var(--space-2xs); + padding: var(--space-xs); + text-align: center; + } +} + .quick-entry-input { width: 100%; padding: 6px 8px; diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index 7adf77505b..04768afda1 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -155,6 +155,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const previousProjectIdRef = useRef(projectId); const [pendingImages, setPendingImages] = useState([]); const pendingImagesRef = useRef([]); + const [isFileDragOver, setIsFileDragOver] = useState(false); + const dragDepthRef = useRef(0); // Rich creation state (mirrors InlineCreateCard) const [dependencies, setDependencies] = useState([]); @@ -609,6 +611,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const resetForm = useCallback(() => { pendingImages.forEach((img) => URL.revokeObjectURL(img.previewUrl)); setPendingImages([]); + dragDepthRef.current = 0; + setIsFileDragOver(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } @@ -650,7 +654,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, } }, [pendingImages, projectId, optionalSteps]); - const handleImageFiles = useCallback((files: FileList | null | undefined) => { + const handleImageFiles = useCallback((files: FileList | File[] | null | undefined) => { if (!files || files.length === 0) return; const newImages: PendingImage[] = []; @@ -666,6 +670,55 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, } }, []); + const isFileDrag = useCallback((dataTransfer: DataTransfer | null) => { + if (!dataTransfer) return false; + return Array.from(dataTransfer.types ?? []).includes("Files"); + }, []); + + /* + FNXC:QuickAddAttachments 2026-06-30-00:00: + Quick Add uses an icon-only paperclip to keep the action row compact, so the accessible name and title carry the Attach action plus pending-image count. The same image intake path handles file input, paste, and file drag/drop so previews and post-create uploads stay consistent. + */ + const attachLabel = pendingImages.length > 0 + ? t("tasks.attachImagesCount", "Attach images ({{count}} pending)", { count: pendingImages.length }) + : t("tasks.attachImages", "Attach images"); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + dragDepthRef.current += 1; + setIsFileDragOver(true); + }, [isFileDrag]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + setIsFileDragOver(true); + }, [isFileDrag]); + + const clearFileDragState = useCallback(() => { + dragDepthRef.current = 0; + setIsFileDragOver(false); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); + if (dragDepthRef.current === 0) { + setIsFileDragOver(false); + } + }, [isFileDrag]); + + const handleDrop = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + clearFileDragState(); + if (isSubmitting) return; + handleImageFiles(e.dataTransfer.files); + }, [clearFileDragState, handleImageFiles, isFileDrag, isSubmitting]); + const handlePaste = useCallback((e: React.ClipboardEvent) => { if (isSubmitting) return; handleImageFiles(e.clipboardData?.files); @@ -1703,7 +1756,21 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, return ( <> -
+
+ {isFileDragOver && ( + + )}
@@ -2164,12 +2231,16 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,