Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings, lists, links, quotes, image upload with drag-and-drop, and themed CSS integration. Add asset image upload API (routes, service, storage) and wire image upload into InlineEditor multiline mode, NewIssueDialog, NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail description fields. Tighten prompt template editor styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useCompany } from "../context/CompanyContext";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { assetsApi } from "../api/assets";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor";
|
||||
import type { Project, Agent } from "@paperclip/shared";
|
||||
|
||||
const DRAFT_KEY = "paperclip:issue-draft";
|
||||
@@ -98,6 +100,7 @@ export function NewIssueDialog() {
|
||||
const [assigneeSearch, setAssigneeSearch] = useState("");
|
||||
const [projectOpen, setProjectOpen] = useState(false);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
@@ -123,6 +126,13 @@ export function NewIssueDialog() {
|
||||
},
|
||||
});
|
||||
|
||||
const uploadDescriptionImage = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
if (!selectedCompanyId) throw new Error("No company selected");
|
||||
return assetsApi.uploadImage(selectedCompanyId, file, "issues/drafts");
|
||||
},
|
||||
});
|
||||
|
||||
// Debounced draft saving
|
||||
const scheduleSave = useCallback(
|
||||
(draft: IssueDraft) => {
|
||||
@@ -263,20 +273,29 @@ export function NewIssueDialog() {
|
||||
placeholder="Issue title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Tab" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
descriptionEditorRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className={cn("px-4 pb-2", expanded ? "flex-1 min-h-0" : "")}>
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full bg-transparent outline-none text-sm text-muted-foreground placeholder:text-muted-foreground/40 resize-none",
|
||||
expanded ? "h-full" : "min-h-[60px]"
|
||||
)}
|
||||
placeholder="Add description..."
|
||||
<MarkdownEditor
|
||||
ref={descriptionEditorRef}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onChange={setDescription}
|
||||
placeholder="Add description..."
|
||||
bordered={false}
|
||||
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-[220px]" : "min-h-[120px]")}
|
||||
imageUploadHandler={async (file) => {
|
||||
const asset = await uploadDescriptionImage.mutateAsync(file);
|
||||
return asset.contentPath;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user