FN-7812: extend select-to-comment popover to Task Documents right pane
Adds select-to-comment parity to the Task Documents right pane in the Artifacts view, reusing the existing Project Files selection-comment pattern (useSelectionComment/SelectionCommentPopover) so operators can highlight task-document content and send it to a new task. - Add markdown/plain preview refs and useSelectionComment hooks scoped to the selected Task Document, following the existing Plain/Markdown render toggle - Gate the Task Documents selection popover on activeTab === "tasks" and the selected task document (separate from the Project Files popover, which stays gated on activeTab === "project") so tab switches never cross-render popovers - Compose the New Task description source as `taskId/key` for task-document selections, mirroring the file-path convention used for Project Files - Add regression tests covering plain/markdown task-document selection, empty-pane gating, tab isolation between Task Documents and Project Files popovers, and the mobile detail pane - Update dashboard-guide.md to document select-to-comment support for Task Documents alongside Project Files - Add a minor changeset for @runfusion/fusion documenting the feature (depends on FN-7811) Files changed: .changeset/fn-7812-task-documents-select-to-comment.md | 7 ++ docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/DocumentsView.tsx | 37 ++++++--- packages/dashboard/app/components/__tests__/DocumentsView.test.tsx | 93 ++++++++++++++++++++++ 4 files changed, 130 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-7812 Fusion-Task-Lineage: ace10d70-35fd-42fe-8a4a-e2c2c7c7c27b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7812-task-documents-select-to-comment.md
Normal file
7
.changeset/fn-7812-task-documents-select-to-comment.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Artifacts view — select text in a Task Document's content pane to comment and send it to a new task.
|
||||||
|
category: feature
|
||||||
|
dev: DocumentsView Task Documents right pane reuses the Project Files `useSelectionComment`/`SelectionCommentPopover` pattern (markdown + plain refs following the render toggle, composer-open lock, popover gated on the task-document selection + `onSendSelectionToTask`). Project Files behavior and the markdown/plain toggle are unchanged. Depends on FN-7811.
|
||||||
@@ -836,7 +836,7 @@ Features:
|
|||||||
- Empty states: with no search query it shows `No artifacts yet.` plus the hint that artifacts are created by agents, users, and system tools; with a search query it shows `No artifacts match "<query>".`
|
- Empty states: with no search query it shows `No artifacts yet.` plus the hint that artifacts are created by agents, users, and system tools; with a search query it shows `No artifacts match "<query>".`
|
||||||
- Error state: a failed artifact list request uses the shared `Failed to load artifacts: <error>` panel with a **Retry** action that re-runs the artifact fetch
|
- Error state: a failed artifact list request uses the shared `Failed to load artifacts: <error>` panel with a **Retry** action that re-runs the artifact fetch
|
||||||
- Toggle between raw text and rendered markdown using the **Markdown/Plain** button
|
- Toggle between raw text and rendered markdown using the **Markdown/Plain** button
|
||||||
- Highlight text in raw or rendered project-file previews, choose **Add comment**, and send the file path, selected snippet, and your comment to the **New Task** dialog
|
- Highlight text in raw or rendered project-file previews or the selected Task Document's right pane, choose **Add comment**, and send the source path/key, selected snippet, and your comment to the **New Task** dialog
|
||||||
|
|
||||||
Agent registrations also surface through the [Mailbox View](#mailbox-view): successful `fn_artifact_register` calls send a best-effort system inbox notification so users can discover new media even before opening the gallery. Artifact list live-refresh does not depend on that best-effort message; it listens to the registry registration event.
|
Agent registrations also surface through the [Mailbox View](#mailbox-view): successful `fn_artifact_register` calls send a best-effort system inbox notification so users can discover new media even before opening the gallery. Artifact list live-refresh does not depend on that best-effort message; it listens to the registry registration event.
|
||||||
|
|
||||||
@@ -871,7 +871,7 @@ Artifacts view supports toggling between raw text and formatted markdown when vi
|
|||||||
|
|
||||||
The toggle button is accessible with `aria-pressed` for screen readers. Toggle state is scoped per-document, so switching between documents resets the view to raw mode.
|
The toggle button is accessible with `aria-pressed` for screen readers. Toggle state is scoped per-document, so switching between documents resets the view to raw mode.
|
||||||
|
|
||||||
Project-file previews also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path, snippet, and comment.
|
Project-file previews and selected Task Documents also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path or task-document key, snippet, and comment.
|
||||||
|
|
||||||
## Todo View
|
## Todo View
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
const requestIdRef = useRef(0);
|
const requestIdRef = useRef(0);
|
||||||
const markdownPreviewRef = useRef<HTMLDivElement>(null);
|
const markdownPreviewRef = useRef<HTMLDivElement>(null);
|
||||||
const plainPreviewRef = useRef<HTMLPreElement>(null);
|
const plainPreviewRef = useRef<HTMLPreElement>(null);
|
||||||
|
const taskDocMarkdownPreviewRef = useRef<HTMLDivElement>(null);
|
||||||
|
const taskDocPlainPreviewRef = useRef<HTMLPreElement>(null);
|
||||||
// Markdown render toggle for project file preview
|
// Markdown render toggle for project file preview
|
||||||
const [renderProjectMarkdown, setRenderProjectMarkdown] = useState(false);
|
const [renderProjectMarkdown, setRenderProjectMarkdown] = useState(false);
|
||||||
// Markdown render toggles per task document card (scoped by doc ID)
|
// Markdown render toggles per task document card (scoped by doc ID)
|
||||||
@@ -81,6 +83,8 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
const [selectionCommentOpen, setSelectionCommentOpen] = useState(false);
|
const [selectionCommentOpen, setSelectionCommentOpen] = useState(false);
|
||||||
const markdownSelection = useSelectionComment(markdownPreviewRef, { locked: selectionCommentOpen });
|
const markdownSelection = useSelectionComment(markdownPreviewRef, { locked: selectionCommentOpen });
|
||||||
const plainSelection = useSelectionComment(plainPreviewRef, { locked: selectionCommentOpen });
|
const plainSelection = useSelectionComment(plainPreviewRef, { locked: selectionCommentOpen });
|
||||||
|
const taskDocMarkdownSelection = useSelectionComment(taskDocMarkdownPreviewRef, { locked: selectionCommentOpen });
|
||||||
|
const taskDocPlainSelection = useSelectionComment(taskDocPlainPreviewRef, { locked: selectionCommentOpen });
|
||||||
const activeProjectSelection = renderProjectMarkdown ? markdownSelection : plainSelection;
|
const activeProjectSelection = renderProjectMarkdown ? markdownSelection : plainSelection;
|
||||||
|
|
||||||
const taskSearchQuery = activeTab === "tasks" ? searchQuery.trim() : "";
|
const taskSearchQuery = activeTab === "tasks" ? searchQuery.trim() : "";
|
||||||
@@ -168,7 +172,10 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:DocumentsView 2026-07-10-17:30:
|
FNXC:DocumentsView 2026-07-10-17:30:
|
||||||
Task Documents now mirrors the Project Files sidebar/right-pane contract: the task-document selection is deliberately separate from selectedFile so tab switching cannot leak project file content into the Task Documents pane. Select-to-comment remains Project-Files-only for FN-7811 and is tracked as follow-up scope; Task Documents preserves only its existing Plain/Markdown render toggle.
|
Task Documents now mirrors the Project Files sidebar/right-pane contract: the task-document selection is deliberately separate from selectedFile so tab switching cannot leak project file content into the Task Documents pane. Task Documents keeps its own Plain/Markdown render toggle so Project Files state never controls task-document rendering.
|
||||||
|
|
||||||
|
FNXC:DocumentsView 2026-07-10-23:41:
|
||||||
|
FN-7812 extends the existing select-to-comment affordance to the Task Documents right pane without a new comment model. The active task-document selection ref follows the same Plain/Markdown toggle, and the composed source path uses taskId/key so operators can identify the originating task document. Keep Task Documents gated by selectedTaskDocument and Project Files gated by selectedFile so tab switches cannot cross-render popovers; the shared composer-open lock is safe because only one tab pane is mounted at a time.
|
||||||
*/
|
*/
|
||||||
const selectedTaskDocument = useMemo(() => {
|
const selectedTaskDocument = useMemo(() => {
|
||||||
if (!selectedTaskDocumentId) {
|
if (!selectedTaskDocumentId) {
|
||||||
@@ -320,7 +327,9 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
}, [activeTab, refreshArtifacts, refreshProjectFiles, refreshDocuments]);
|
}, [activeTab, refreshArtifacts, refreshProjectFiles, refreshDocuments]);
|
||||||
|
|
||||||
const activeCount = activeTab === "project" ? filteredProjectFiles.length : activeTab === "tasks" ? documents.length : artifacts.length;
|
const activeCount = activeTab === "project" ? filteredProjectFiles.length : activeTab === "tasks" ? documents.length : artifacts.length;
|
||||||
const selectionPopover = selectedFile && onSendSelectionToTask && activeProjectSelection ? (
|
const selectedTaskDocumentRendersMarkdown = selectedTaskDocument ? (taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) : false;
|
||||||
|
const activeTaskDocumentSelection = selectedTaskDocumentRendersMarkdown ? taskDocMarkdownSelection : taskDocPlainSelection;
|
||||||
|
const selectionPopover = activeTab === "project" && selectedFile && onSendSelectionToTask && activeProjectSelection ? (
|
||||||
<SelectionCommentPopover
|
<SelectionCommentPopover
|
||||||
selectedText={activeProjectSelection.selectedText}
|
selectedText={activeProjectSelection.selectedText}
|
||||||
anchorRect={activeProjectSelection.anchorRect}
|
anchorRect={activeProjectSelection.anchorRect}
|
||||||
@@ -329,6 +338,15 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
onOpenChange={setSelectionCommentOpen}
|
onOpenChange={setSelectionCommentOpen}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
const taskDocumentSelectionPopover = activeTab === "tasks" && selectedTaskDocument && onSendSelectionToTask && activeTaskDocumentSelection ? (
|
||||||
|
<SelectionCommentPopover
|
||||||
|
selectedText={activeTaskDocumentSelection.selectedText}
|
||||||
|
anchorRect={activeTaskDocumentSelection.anchorRect}
|
||||||
|
filePath={`${selectedTaskDocument.taskId}/${selectedTaskDocument.key}`}
|
||||||
|
onSubmit={onSendSelectionToTask}
|
||||||
|
onOpenChange={setSelectionCommentOpen}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const searchPlaceholder = activeTab === "project"
|
const searchPlaceholder = activeTab === "project"
|
||||||
? t("documents.searchProjectFiles", "Search project markdown files…")
|
? t("documents.searchProjectFiles", "Search project markdown files…")
|
||||||
@@ -691,22 +709,23 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sm document-mode-toggle"
|
className="btn btn-sm document-mode-toggle"
|
||||||
onClick={() => handleToggleTaskDocMarkdown(selectedTaskDocument.id)}
|
onClick={() => handleToggleTaskDocMarkdown(selectedTaskDocument.id)}
|
||||||
aria-label={(taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) ? t("documents.switchToPlainText", "Switch to plain text") : t("documents.switchToMarkdown", "Switch to markdown")}
|
aria-label={selectedTaskDocumentRendersMarkdown ? t("documents.switchToPlainText", "Switch to plain text") : t("documents.switchToMarkdown", "Switch to markdown")}
|
||||||
aria-pressed={taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false}
|
aria-pressed={selectedTaskDocumentRendersMarkdown}
|
||||||
title={(taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) ? t("documents.switchToPlainText", "Switch to plain text") : t("documents.switchToMarkdown", "Switch to markdown")}
|
title={selectedTaskDocumentRendersMarkdown ? t("documents.switchToPlainText", "Switch to plain text") : t("documents.switchToMarkdown", "Switch to markdown")}
|
||||||
>
|
>
|
||||||
{(taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) ? t("documents.markdown", "Markdown") : t("documents.plain", "Plain")}
|
{selectedTaskDocumentRendersMarkdown ? t("documents.markdown", "Markdown") : t("documents.plain", "Plain")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) ? (
|
{selectedTaskDocumentRendersMarkdown ? (
|
||||||
<div className="documents-content-markdown">
|
<div ref={taskDocMarkdownPreviewRef} className="documents-content-markdown">
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedTaskDocument.content}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedTaskDocument.content}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<pre className="document-card-content-text documents-content-viewer-text">{selectedTaskDocument.content}</pre>
|
<pre ref={taskDocPlainPreviewRef} className="document-card-content-text documents-content-viewer-text">{selectedTaskDocument.content}</pre>
|
||||||
)}
|
)}
|
||||||
|
{taskDocumentSelectionPopover}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ describe("DocumentsView", () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.innerWidth = originalInnerWidth;
|
window.innerWidth = originalInnerWidth;
|
||||||
|
document.getSelection()?.removeAllRanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -834,6 +835,98 @@ describe("DocumentsView", () => {
|
|||||||
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Review this rendered content."));
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Review this rendered content."));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:DocumentsView 2026-07-10-23:46:
|
||||||
|
FN-7812 adds select-to-comment parity to Task Documents, so tests must cover the same surface checklist as Project Files: plain and markdown render modes, desktop and mobile detail layouts, empty-pane gating, tab isolation, and task-document file context in the composed New Task description.
|
||||||
|
*/
|
||||||
|
it("sends selected plain task document text to a new task description", async () => {
|
||||||
|
mockSelectionRect();
|
||||||
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} onSendSelectionToTask={onSendSelectionToTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show task documents/i }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" }));
|
||||||
|
const plainPreview = screen.getByText("Alpha document content");
|
||||||
|
selectNodeText(plainPreview);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: /add a comment/i }));
|
||||||
|
fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Follow up from the task doc." } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /send to new task/i }));
|
||||||
|
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan"));
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Alpha document content"));
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Follow up from the task doc."));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends selected markdown task document text to a new task description", async () => {
|
||||||
|
mockSelectionRect();
|
||||||
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} onSendSelectionToTask={onSendSelectionToTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show task documents/i }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
|
||||||
|
const markdownPreviewText = await screen.findByText("Alpha document content");
|
||||||
|
selectNodeText(markdownPreviewText);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: /add a comment/i }));
|
||||||
|
fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Review rendered task doc." } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /send to new task/i }));
|
||||||
|
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan"));
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Alpha document content"));
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Review rendered task doc."));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show the task document comment trigger in the empty right pane", () => {
|
||||||
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} onSendSelectionToTask={onSendSelectionToTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show task documents/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText("Select a task document to view its content.")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: /add a comment/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps task document and project file selection comment popovers isolated", async () => {
|
||||||
|
mockSelectionRect();
|
||||||
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} onSendSelectionToTask={onSendSelectionToTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show task documents/i }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" }));
|
||||||
|
selectNodeText(screen.getByText("Alpha document content"));
|
||||||
|
expect(await screen.findByRole("button", { name: /add a comment/i })).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i }));
|
||||||
|
expect(screen.queryByRole("button", { name: /add a comment/i })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open README.md" }));
|
||||||
|
const projectPreview = await screen.findByText(/Hello docs/);
|
||||||
|
selectNodeText(projectPreview);
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: /add a comment/i }));
|
||||||
|
fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Project file still works." } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /send to new task/i }));
|
||||||
|
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: README.md"));
|
||||||
|
expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Project file still works."));
|
||||||
|
expect(onSendSelectionToTask).not.toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the task document comment trigger in the mobile detail pane", async () => {
|
||||||
|
window.innerWidth = 600;
|
||||||
|
mockSelectionRect();
|
||||||
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} onSendSelectionToTask={onSendSelectionToTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /show task documents/i }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" }));
|
||||||
|
const mobilePreview = screen.getByText("Alpha document content");
|
||||||
|
selectNodeText(mobilePreview);
|
||||||
|
|
||||||
|
expect(await screen.findByRole("button", { name: /add a comment/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("search filters task documents and clears filtered-out selection", async () => {
|
it("search filters task documents and clears filtered-out selection", async () => {
|
||||||
mockUseProjectMarkdownFiles.mockReturnValue({
|
mockUseProjectMarkdownFiles.mockReturnValue({
|
||||||
files: [],
|
files: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user