import { useCallback, useEffect, useState } from "react"; import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History, X } from "lucide-react"; import type { Task, TaskDocument, TaskDocumentRevision } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { fetchTaskDocuments, fetchTaskDocument, fetchTaskDocumentRevisions, putTaskDocument, deleteTaskDocument, } from "../api"; // Document key validation: alphanumeric, hyphens, underscores, 1-64 chars const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; const MAX_CONTENT_PREVIEW = 200; interface TaskDocumentsTabProps { taskId: string; addToast: (message: string, type?: ToastType) => void; onTaskUpdated?: (task: Task) => void; projectId?: string; canEdit?: boolean; } function formatTimestamp(iso?: string): string { if (!iso) return ""; return new Date(iso).toLocaleString(); } function getContentPreview(content: string, maxLength: number = MAX_CONTENT_PREVIEW): string { if (content.length <= maxLength) return content; return content.substring(0, maxLength) + "…"; } export function TaskDocumentsTab({ taskId, addToast, onTaskUpdated, projectId, canEdit = false, }: TaskDocumentsTabProps) { const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [expandedDocKey, setExpandedDocKey] = useState(null); const [expandedContent, setExpandedContent] = useState(""); const [editingDocKey, setEditingDocKey] = useState(null); const [editContent, setEditContent] = useState(""); const [showHistory, setShowHistory] = useState(null); const [revisions, setRevisions] = useState([]); const [loadingRevisions, setLoadingRevisions] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false); const [newDocKey, setNewDocKey] = useState(""); const [newDocContent, setNewDocContent] = useState(""); const [saving, setSaving] = useState(false); const [deletingKey, setDeletingKey] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const loadDocuments = useCallback(async () => { try { const docs = await fetchTaskDocuments(taskId, projectId); setDocuments(docs); } catch (error: any) { addToast(error.message || "Failed to load documents", "error"); } finally { setLoading(false); } }, [taskId, projectId, addToast]); useEffect(() => { void loadDocuments(); }, [loadDocuments]); async function handleExpandDocument(doc: TaskDocument) { if (expandedDocKey === doc.key) { setExpandedDocKey(null); setExpandedContent(""); setEditingDocKey(null); setEditContent(""); setShowHistory(null); setRevisions([]); } else { setExpandedDocKey(doc.key); setExpandedContent(doc.content); setEditingDocKey(null); setEditContent(""); setShowHistory(null); setRevisions([]); } } async function handleToggleHistory(docKey: string) { if (showHistory === docKey) { setShowHistory(null); setRevisions([]); } else { setShowHistory(docKey); setLoadingRevisions(true); try { const revs = await fetchTaskDocumentRevisions(taskId, docKey, projectId); setRevisions(revs); } catch (error: any) { addToast(error.message || "Failed to load revisions", "error"); } finally { setLoadingRevisions(false); } } } function handleStartEdit() { if (expandedDocKey) { setEditingDocKey(expandedDocKey); setEditContent(expandedContent); } } function handleCancelEdit() { setEditingDocKey(null); setEditContent(""); } async function handleSaveEdit() { if (!editingDocKey || !editContent.trim()) return; setSaving(true); try { await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId); setEditingDocKey(null); setEditContent(""); await loadDocuments(); // Refresh expanded content const updated = documents.find((d) => d.key === editingDocKey); if (updated) { setExpandedContent(updated.content); } addToast("Document saved", "success"); } catch (error: any) { addToast(error.message || "Failed to save document", "error"); } finally { setSaving(false); } } async function handleCreateDocument() { const key = newDocKey.trim(); const content = newDocContent.trim(); // Validate key if (!key) { addToast("Document key is required", "error"); return; } if (!DOCUMENT_KEY_REGEX.test(key)) { addToast("Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores.", "error"); return; } if (!content) { addToast("Content is required", "error"); return; } setSaving(true); try { await putTaskDocument(taskId, key, content, {}, projectId); setShowCreateForm(false); setNewDocKey(""); setNewDocContent(""); await loadDocuments(); addToast("Document created", "success"); } catch (error: any) { addToast(error.message || "Failed to create document", "error"); } finally { setSaving(false); } } async function handleDeleteDocument(key: string) { setDeletingKey(key); try { await deleteTaskDocument(taskId, key, projectId); setConfirmDelete(null); setDeletingKey(null); if (expandedDocKey === key) { setExpandedDocKey(null); setExpandedContent(""); } if (showHistory === key) { setShowHistory(null); setRevisions([]); } await loadDocuments(); addToast("Document deleted", "success"); } catch (error: any) { addToast(error.message || "Failed to delete document", "error"); } finally { setDeletingKey(null); } } function handleViewRevision(revision: TaskDocumentRevision) { setExpandedContent(revision.content); setEditingDocKey(null); setEditContent(""); } if (loading) { return (

Documents

Loading documents…
); } return (

Documents

{/* Create Form */} {showCreateForm && (
New Document
setNewDocKey(e.target.value)} placeholder="e.g., plan, notes, research" disabled={saving} /> Alphanumeric, hyphens, underscores (1-64 chars)