feat(FN-1271): add Documents tab to task detail modal
- Add task document API routes for listing, viewing, and downloading task files - Create TaskDocumentsTab component with file browser UI - Integrate Documents tab into TaskDetailModal alongside existing tabs - Add frontend API functions for task document operations - Add CSS styles for document list, file icons, and download actions - Add comprehensive route tests for task document endpoints
This commit is contained in:
@@ -16,6 +16,7 @@ import { MergeDetails } from "./MergeDetails";
|
||||
import { TaskChangesTab } from "./TaskChangesTab";
|
||||
import { TaskForm, type PendingImage } from "./TaskForm";
|
||||
import { WorkflowResultsTab } from "./WorkflowResultsTab";
|
||||
import { TaskDocumentsTab } from "./TaskDocumentsTab";
|
||||
|
||||
interface ModelSelection {
|
||||
provider?: string;
|
||||
@@ -166,7 +167,7 @@ function formatBytes(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
type TabId = "definition" | "logs" | "changes" | "comments" | "model" | "workflow";
|
||||
type TabId = "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "documents";
|
||||
|
||||
interface TaskDetailModalProps {
|
||||
task: Task | TaskDetail;
|
||||
@@ -1094,6 +1095,12 @@ export function TaskDetailModal({
|
||||
>
|
||||
Comments
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "documents" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("documents")}
|
||||
>
|
||||
Documents
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "model" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("model")}
|
||||
@@ -1176,6 +1183,14 @@ export function TaskDetailModal({
|
||||
<TaskChangesTab taskId={task.id} worktree={task.worktree} projectId={projectId} column={task.column} mergeDetails={task.mergeDetails} />
|
||||
) : activeTab === "comments" ? (
|
||||
<TaskComments task={task} addToast={addToast} projectId={projectId} onTaskUpdated={onTaskUpdated} />
|
||||
) : activeTab === "documents" ? (
|
||||
<TaskDocumentsTab
|
||||
taskId={task.id}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
onTaskUpdated={onTaskUpdated}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Summary section - only for done tasks with summary */}
|
||||
|
||||
442
packages/dashboard/app/components/TaskDocumentsTab.tsx
Normal file
442
packages/dashboard/app/components/TaskDocumentsTab.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
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<TaskDocument[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedDocKey, setExpandedDocKey] = useState<string | null>(null);
|
||||
const [expandedContent, setExpandedContent] = useState("");
|
||||
const [editingDocKey, setEditingDocKey] = useState<string | null>(null);
|
||||
const [editContent, setEditContent] = useState("");
|
||||
const [showHistory, setShowHistory] = useState<string | null>(null);
|
||||
const [revisions, setRevisions] = useState<TaskDocumentRevision[]>([]);
|
||||
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<string | null>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(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 (
|
||||
<div className="detail-section">
|
||||
<h4>Documents</h4>
|
||||
<div className="detail-log-empty">Loading documents…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<h4>Documents</h4>
|
||||
|
||||
{/* Create Form */}
|
||||
{showCreateForm && (
|
||||
<div className="task-document-create-form">
|
||||
<h5>New Document</h5>
|
||||
<div className="form-group">
|
||||
<label htmlFor="doc-key">Key</label>
|
||||
<input
|
||||
id="doc-key"
|
||||
type="text"
|
||||
className="task-document-key-input"
|
||||
value={newDocKey}
|
||||
onChange={(e) => setNewDocKey(e.target.value)}
|
||||
placeholder="e.g., plan, notes, research"
|
||||
disabled={saving}
|
||||
/>
|
||||
<span className="form-hint">Alphanumeric, hyphens, underscores (1-64 chars)</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="doc-content">Content</label>
|
||||
<textarea
|
||||
id="doc-content"
|
||||
value={newDocContent}
|
||||
onChange={(e) => setNewDocContent(e.target.value)}
|
||||
rows={6}
|
||||
placeholder="Enter document content…"
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => {
|
||||
setShowCreateForm(false);
|
||||
setNewDocKey("");
|
||||
setNewDocContent("");
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => void handleCreateDocument()}
|
||||
disabled={saving || !newDocKey.trim() || !newDocContent.trim()}
|
||||
>
|
||||
{saving ? "Creating…" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Document List */}
|
||||
{documents.length === 0 && !showCreateForm ? (
|
||||
<div className="detail-log-empty">
|
||||
No documents yet.
|
||||
{canEdit && (
|
||||
<button
|
||||
className="btn btn-link"
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
Create one
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="task-documents-list">
|
||||
{documents.map((doc) => (
|
||||
<div key={doc.key} className="task-document-card">
|
||||
<div className="task-document-card-header">
|
||||
<div className="task-document-card-title">
|
||||
<FileText size={14} />
|
||||
<span className="task-document-key">{doc.key}</span>
|
||||
<span className="task-document-revision-badge">v{doc.revision}</span>
|
||||
</div>
|
||||
<div className="task-document-meta">
|
||||
<span className="task-document-author">{doc.author}</span>
|
||||
<span className="task-document-timestamp">{formatTimestamp(doc.updatedAt || doc.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content View */}
|
||||
{expandedDocKey === doc.key && editingDocKey !== doc.key && (
|
||||
<>
|
||||
<div className="task-document-content">
|
||||
<pre className="task-document-content-text">{expandedContent}</pre>
|
||||
</div>
|
||||
|
||||
{/* Revision History */}
|
||||
{showHistory === doc.key && (
|
||||
<div className="task-document-revisions">
|
||||
<h5>Revision History</h5>
|
||||
{loadingRevisions ? (
|
||||
<div className="detail-log-empty">Loading…</div>
|
||||
) : revisions.length <= 1 ? (
|
||||
<div className="detail-log-empty">No previous revisions.</div>
|
||||
) : (
|
||||
<div className="task-document-revision-list">
|
||||
{revisions
|
||||
.filter((r) => r.revision < doc.revision)
|
||||
.sort((a, b) => b.revision - a.revision)
|
||||
.map((revision) => (
|
||||
<div
|
||||
key={revision.id}
|
||||
className="task-document-revision-item"
|
||||
onClick={() => handleViewRevision(revision)}
|
||||
>
|
||||
<div className="revision-header">
|
||||
<span className="revision-badge">v{revision.revision}</span>
|
||||
<span className="revision-author">{revision.author}</span>
|
||||
<span className="revision-timestamp">{formatTimestamp(revision.createdAt)}</span>
|
||||
</div>
|
||||
<div className="revision-preview">{getContentPreview(revision.content, 100)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Editing View */}
|
||||
{editingDocKey === doc.key && (
|
||||
<div className="task-document-edit-form">
|
||||
<textarea
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
rows={10}
|
||||
disabled={saving}
|
||||
/>
|
||||
<div className="form-actions">
|
||||
<button className="btn btn-sm" onClick={handleCancelEdit} disabled={saving}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => void handleSaveEdit()}
|
||||
disabled={saving || !editContent.trim()}
|
||||
>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="task-document-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleExpandDocument(doc)}
|
||||
>
|
||||
{expandedDocKey === doc.key ? (
|
||||
<>
|
||||
<ChevronUp size={14} /> Collapse
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown size={14} /> Expand
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedDocKey === doc.key && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleToggleHistory(doc.key)}
|
||||
>
|
||||
<History size={14} /> History
|
||||
</button>
|
||||
|
||||
{canEdit && editingDocKey !== doc.key && (
|
||||
<button className="btn btn-sm" onClick={handleStartEdit}>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canEdit && (
|
||||
confirmDelete === doc.key ? (
|
||||
<div className="confirm-delete-actions">
|
||||
<span>Delete?</span>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => void handleDeleteDocument(doc.key)}
|
||||
disabled={deletingKey === doc.key}
|
||||
>
|
||||
{deletingKey === doc.key ? "…" : "Yes"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
disabled={deletingKey === doc.key}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => setConfirmDelete(doc.key)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New Document Button */}
|
||||
{canEdit && !showCreateForm && (
|
||||
<button
|
||||
className="btn btn-sm task-document-new-btn"
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
>
|
||||
<Plus size={14} /> New Document
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1055,21 +1055,22 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
// For an in-progress task (no workflow steps, no merge commit),
|
||||
// the top-level tabs are: Definition, Logs, Changes, Comments, Model, Workflow
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Model", "Workflow"];
|
||||
// the top-level tabs are: Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow"];
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
tabTexts.includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
|
||||
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(6);
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(7);
|
||||
// Workflow tab should always appear even when no workflow steps are configured
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
// Commits tab should NOT appear for non-done tasks
|
||||
@@ -1968,7 +1969,7 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6); // Definition, Logs, Changes, Comments, Model, Workflow
|
||||
expect(tabs.length).toBe(7); // Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
// Tabs should use class-based styling, not inline styles
|
||||
expect(tabs[0].classList.contains("detail-tab")).toBe(true);
|
||||
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); // Definition is default active
|
||||
@@ -1977,6 +1978,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[3].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[4].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[5].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[6].classList.contains("detail-tab-active")).toBe(false);
|
||||
// Verify no inline padding/fontSize (responsive CSS controls this)
|
||||
expect((tabs[0] as HTMLElement).style.padding).toBe("");
|
||||
expect((tabs[0] as HTMLElement).style.fontSize).toBe("");
|
||||
@@ -2587,16 +2589,17 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress tasks show exactly 6 tabs:
|
||||
// Definition, Logs, Changes, Comments, Model, Workflow
|
||||
// In-progress tasks show exactly 7 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
// Commits tab should NOT be present for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -2614,15 +2617,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress task with workflow steps: 6 tabs (Workflow after Model)
|
||||
// In-progress task with workflow steps: 7 tabs (Workflow after Model)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
});
|
||||
|
||||
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
|
||||
@@ -2641,15 +2645,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Model, Workflow (6 tabs, no Commits)
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow (7 tabs, no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -2671,15 +2676,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with workflow steps and commit SHA: 6 tabs (no Commits)
|
||||
// Done task with workflow steps and commit SHA: 7 tabs (no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -2698,9 +2704,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||
expect(triageTabs.length).toBe(5); // Definition, Logs, Comments, Model, Workflow
|
||||
expect(triageTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
|
||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Model", "Workflow",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
|
||||
]);
|
||||
|
||||
const { container: todoContainer } = render(
|
||||
@@ -2716,9 +2722,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||
expect(todoTabs.length).toBe(5); // Definition, Logs, Comments, Model, Workflow
|
||||
expect(todoTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
|
||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Model", "Workflow",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -4257,7 +4263,7 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
// Changes tab should exist and be available
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6); // Definition, Logs, Changes, Comments, Model, Workflow
|
||||
expect(tabs.length).toBe(7); // Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
const tabTexts = Array.from(tabs).map((t) => t.textContent);
|
||||
expect(tabTexts).toContain("Changes");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user