FN-5801: add markdown preview toggle for planning description
Add a markdown/plain toggle to the planning summary description so users can preview formatted content before creating a task. - add a Description header toggle that switches between editable plain textarea and rendered markdown preview - render description preview with react-markdown and remark-gfm while preserving expand/collapse layout behavior - add preview container styles and spacing adjustments for summary form - add UI interaction test coverage for markdown toggle behavior and rendered heading/bold content - document the new markdown/plain description toggle in dashboard guide Files changed: docs/dashboard-guide.md | 1 + packages/dashboard/app/components/PlanningModeModal.css | 18 +++++++- packages/dashboard/app/components/PlanningModeModal.tsx | 32 ++++++++++--- packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx | 54 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5801 Fusion-Task-Lineage: 34972830-7d51-4fcd-bf00-3de8f5aff028
This commit is contained in:
@@ -109,6 +109,7 @@ Planning Mode now includes branch controls on the summary screen before you crea
|
||||
- `Create custom new branch`
|
||||
- **Branch name** is required when using `existing` or `custom new` strategies.
|
||||
- **Merge target / base branch (optional)** uses a dropdown of existing local branches (with common names like `main`/`master`/`trunk`/`develop` listed first) plus a **Custom…** fallback when you need to type a branch that is not local yet.
|
||||
- **Description** supports a `Markdown`/`Plain` toggle in the summary header row: `Plain` keeps the editable textarea, while `Markdown` renders formatted preview (`react-markdown` + GFM) in the same footprint for easier review before task creation.
|
||||
|
||||
These values are sent with the Planning Mode create-task request as `branchSelection`, so created tasks persist branch/base-branch settings consistently with other branch-aware task creation flows.
|
||||
|
||||
|
||||
@@ -606,6 +606,22 @@
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.planning-description-preview {
|
||||
width: 100%;
|
||||
min-height: calc(var(--space-md) * 7.5);
|
||||
max-height: calc(var(--space-2xl) * 20);
|
||||
padding: var(--space-md);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.planning-description-preview.expanded {
|
||||
min-height: calc(var(--space-md) * 12.5);
|
||||
}
|
||||
|
||||
.planning-comment-section {
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
@@ -940,7 +956,7 @@
|
||||
.planning-summary-form .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-lg);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "./PlanningModeModal.css";
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Task, PlanningQuestion, PlanningSummary, TaskPriority } from "@fusion/core";
|
||||
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
|
||||
import {
|
||||
@@ -2422,6 +2424,7 @@ function SummaryView({
|
||||
isLoading,
|
||||
}: SummaryViewProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [renderMarkdown, setRenderMarkdown] = useState(false);
|
||||
const [selectedDependencies, setSelectedDependencies] = useState<string[]>(
|
||||
summary.suggestedDependencies
|
||||
);
|
||||
@@ -2463,6 +2466,17 @@ function SummaryView({
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Description
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
aria-pressed={renderMarkdown}
|
||||
aria-label={renderMarkdown ? "Show raw text" : "Show formatted markdown"}
|
||||
title={renderMarkdown ? "Show raw text" : "Show formatted markdown"}
|
||||
data-testid="planning-description-markdown-toggle"
|
||||
onClick={() => setRenderMarkdown(!renderMarkdown)}
|
||||
>
|
||||
{renderMarkdown ? "Plain" : "Markdown"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
@@ -2471,12 +2485,18 @@ function SummaryView({
|
||||
{isExpanded ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</label>
|
||||
<textarea
|
||||
ref={descriptionAutosizeRef}
|
||||
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
|
||||
value={summary.description}
|
||||
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })}
|
||||
/>
|
||||
{renderMarkdown ? (
|
||||
<div className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
ref={descriptionAutosizeRef}
|
||||
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
|
||||
value={summary.description}
|
||||
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="task-detail-section">
|
||||
|
||||
@@ -728,4 +728,58 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Summary markdown preview toggle", () => {
|
||||
it("toggles description between plain textarea and formatted markdown preview", async () => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onSummary?.({
|
||||
...mockSummary,
|
||||
description: "## Heading\n\n- item\n\n**bold**",
|
||||
});
|
||||
}, 10);
|
||||
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
onTaskCreated={mockOnTaskCreated}
|
||||
onTasksCreated={vi.fn()}
|
||||
tasks={mockTasks}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||
target: { value: "Build auth system" },
|
||||
});
|
||||
fireEvent.click(screen.getByText("Start Planning"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(container.querySelector(".planning-textarea")).not.toBeNull();
|
||||
expect(container.querySelector(".planning-description-preview")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId("planning-description-markdown-toggle"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".planning-description-preview")).not.toBeNull();
|
||||
expect(screen.getByRole("heading", { level: 2, name: "Heading" })).toBeDefined();
|
||||
expect(container.querySelector("strong")?.textContent).toBe("bold");
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("planning-description-markdown-toggle"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".planning-textarea")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user