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`
|
- `Create custom new branch`
|
||||||
- **Branch name** is required when using `existing` or `custom new` strategies.
|
- **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.
|
- **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.
|
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);
|
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 {
|
.planning-comment-section {
|
||||||
margin-top: var(--space-lg);
|
margin-top: var(--space-lg);
|
||||||
}
|
}
|
||||||
@@ -940,7 +956,7 @@
|
|||||||
.planning-summary-form .form-group {
|
.planning-summary-form .form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: var(--space-sm);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import "./PlanningModeModal.css";
|
import "./PlanningModeModal.css";
|
||||||
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
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 type { Task, PlanningQuestion, PlanningSummary, TaskPriority } from "@fusion/core";
|
||||||
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
|
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
@@ -2422,6 +2424,7 @@ function SummaryView({
|
|||||||
isLoading,
|
isLoading,
|
||||||
}: SummaryViewProps) {
|
}: SummaryViewProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [renderMarkdown, setRenderMarkdown] = useState(false);
|
||||||
const [selectedDependencies, setSelectedDependencies] = useState<string[]>(
|
const [selectedDependencies, setSelectedDependencies] = useState<string[]>(
|
||||||
summary.suggestedDependencies
|
summary.suggestedDependencies
|
||||||
);
|
);
|
||||||
@@ -2463,6 +2466,17 @@ function SummaryView({
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<label>
|
||||||
Description
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="planning-expand-btn"
|
className="planning-expand-btn"
|
||||||
@@ -2471,12 +2485,18 @@ function SummaryView({
|
|||||||
{isExpanded ? "Collapse" : "Expand"}
|
{isExpanded ? "Collapse" : "Expand"}
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
{renderMarkdown ? (
|
||||||
ref={descriptionAutosizeRef}
|
<div className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}>
|
||||||
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown>
|
||||||
value={summary.description}
|
</div>
|
||||||
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })}
|
) : (
|
||||||
/>
|
<textarea
|
||||||
|
ref={descriptionAutosizeRef}
|
||||||
|
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
|
||||||
|
value={summary.description}
|
||||||
|
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="task-detail-section">
|
<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