fix: sync create mode toolbar options

This commit is contained in:
Devv
2026-04-29 21:39:47 +08:00
parent f1441c4609
commit 4a5f0d9874
3 changed files with 124 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ const mockCreateIssue = vi.hoisted(() => vi.fn());
const mockSetDraft = vi.hoisted(() => vi.fn());
const mockClearDraft = vi.hoisted(() => vi.fn());
const mockSetLastAssignee = vi.hoisted(() => vi.fn());
const mockSetKeepOpen = vi.hoisted(() => vi.fn());
const mockToastCustom = vi.hoisted(() => vi.fn());
const mockToastDismiss = vi.hoisted(() => vi.fn());
const mockToastError = vi.hoisted(() => vi.fn());
@@ -30,6 +31,11 @@ const mockDraftStore = {
setLastAssignee: mockSetLastAssignee,
};
const mockQuickCreateStore = {
keepOpen: false,
setKeepOpen: mockSetKeepOpen,
};
vi.mock("../navigation", () => ({
useNavigation: () => ({ push: mockPush }),
}));
@@ -60,6 +66,11 @@ vi.mock("@multica/core/issues/stores/draft-store", () => ({
),
}));
vi.mock("@multica/core/issues/stores/quick-create-store", () => ({
useQuickCreateStore: (selector?: (state: typeof mockQuickCreateStore) => unknown) =>
(selector ? selector(mockQuickCreateStore) : mockQuickCreateStore),
}));
vi.mock("@multica/core/issues/mutations", () => ({
useCreateIssue: () => ({ mutateAsync: mockCreateIssue }),
useUpdateIssue: () => ({ mutate: vi.fn() }),
@@ -79,6 +90,10 @@ vi.mock("../editor", () => {
const [value, setValue] = useState(defaultValue || "");
useImperativeHandle(ref, () => ({
getMarkdown: () => valueRef.current,
clearContent: () => {
valueRef.current = "";
setValue("");
},
uploadFile: vi.fn(),
}));
return (
@@ -178,6 +193,23 @@ vi.mock("@multica/ui/components/ui/button", () => ({
),
}));
vi.mock("@multica/ui/components/ui/switch", () => ({
Switch: ({
checked,
onCheckedChange,
}: {
checked: boolean;
onCheckedChange: (v: boolean) => void;
}) => (
<input
aria-label="Create another"
type="checkbox"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
/>
),
}));
vi.mock("@multica/ui/components/common/file-upload-button", () => ({
FileUploadButton: ({ onSelect }: { onSelect: (file: File) => void }) => (
<button type="button" onClick={() => onSelect(new File(["test"], "test.txt"))}>
@@ -210,6 +242,10 @@ function renderModal(element: React.ReactElement) {
describe("CreateIssueModal", () => {
beforeEach(() => {
vi.clearAllMocks();
mockQuickCreateStore.keepOpen = false;
mockSetKeepOpen.mockImplementation((v: boolean) => {
mockQuickCreateStore.keepOpen = v;
});
mockCreateIssue.mockResolvedValue({
id: "issue-123",
identifier: "TES-123",
@@ -261,4 +297,44 @@ describe("CreateIssueModal", () => {
expect(mockPush).toHaveBeenCalledWith("/ws-test/issues/issue-123");
expect(mockToastDismiss).toHaveBeenCalledWith("toast-1");
});
it("keeps manual mode open and clears content when create another is enabled", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
mockQuickCreateStore.keepOpen = true;
renderModal(<CreateIssueModal onClose={onClose} />);
await user.type(screen.getByPlaceholderText("Issue title"), "First follow-up issue");
await user.type(screen.getByPlaceholderText("Add description..."), "Description to clear");
await user.click(screen.getByRole("button", { name: "Create Issue" }));
await waitFor(() => {
expect(mockCreateIssue).toHaveBeenCalledWith({
title: "First follow-up issue",
description: "Description to clear",
status: "todo",
priority: "none",
assignee_type: undefined,
assignee_id: undefined,
due_date: undefined,
attachment_ids: undefined,
parent_issue_id: undefined,
project_id: undefined,
});
});
expect(onClose).not.toHaveBeenCalled();
expect(screen.getByPlaceholderText("Issue title")).toHaveValue("");
expect(screen.getByPlaceholderText("Add description...")).toHaveValue("");
expect(mockSetDraft).toHaveBeenCalledWith({
title: "",
description: "",
status: "todo",
priority: "none",
assigneeType: undefined,
assigneeId: undefined,
dueDate: null,
});
});
});

View File

@@ -30,6 +30,7 @@ import {
} from "@multica/ui/components/ui/dropdown-menu";
import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
import { Button } from "@multica/ui/components/ui/button";
import { Switch } from "@multica/ui/components/ui/switch";
import { ContentEditor, type ContentEditorRef, TitleEditor, useFileDropZone, FileDropOverlay } from "../editor";
import { StatusIcon, StatusPicker, PriorityPicker, AssigneePicker, DueDatePicker } from "../issues/components";
import { BacklogAgentHintContent } from "../issues/components/backlog-agent-hint-dialog";
@@ -38,6 +39,7 @@ import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths";
import { useWorkspaceId } from "@multica/core/hooks";
import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store";
import { useCreateModeStore } from "@multica/core/issues/stores/create-mode-store";
import { useQuickCreateStore } from "@multica/core/issues/stores/quick-create-store";
import { issueDetailOptions } from "@multica/core/issues/queries";
import { useCreateIssue, useUpdateIssue } from "@multica/core/issues/mutations";
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
@@ -84,8 +86,11 @@ export function ManualCreatePanel({
const clearDraft = useIssueDraftStore((s) => s.clearDraft);
const setLastAssignee = useIssueDraftStore((s) => s.setLastAssignee);
const setLastMode = useCreateModeStore((s) => s.setLastMode);
const keepOpen = useQuickCreateStore((s) => s.keepOpen);
const setKeepOpen = useQuickCreateStore((s) => s.setKeepOpen);
const [title, setTitle] = useState(draft.title);
const [formResetKey, setFormResetKey] = useState(0);
const descEditorRef = useRef<ContentEditorRef>(null);
const { isDragOver: descDragOver, dropZoneProps: descDropZoneProps } = useFileDropZone({
onDrop: (files) => files.forEach((f) => descEditorRef.current?.uploadFile(f)),
@@ -138,6 +143,28 @@ export function ManualCreatePanel({
const createIssueMutation = useCreateIssue();
const updateIssueMutation = useUpdateIssue();
const resetForNextIssue = () => {
setTitle("");
setStatus("todo");
setPriority("none");
setDueDate(null);
setProjectId(undefined);
setParentIssueId(undefined);
setChildIssues([]);
setAttachmentIds([]);
setDraft({
title: "",
description: "",
status: "todo",
priority: "none",
assigneeType,
assigneeId,
dueDate: null,
});
descEditorRef.current?.clearContent();
setFormResetKey((key) => key + 1);
};
const handleSubmit = async () => {
if (!title.trim() || submitting) return;
setSubmitting(true);
@@ -186,6 +213,8 @@ export function ManualCreatePanel({
if (shouldShowBacklogHint) {
setBacklogHintIssueId(issue.id);
} else if (keepOpen) {
resetForNextIssue();
} else {
onClose();
}
@@ -304,6 +333,7 @@ export function ManualCreatePanel({
{/* Title */}
<div className="px-5 pb-2 shrink-0">
<TitleEditor
key={formResetKey}
autoFocus
defaultValue={draft.title}
placeholder="Issue title"
@@ -494,20 +524,30 @@ export function ManualCreatePanel({
/>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-3 border-t shrink-0">
<FileUploadButton
onSelect={(file) => descEditorRef.current?.uploadFile(file)}
/>
<div className="flex items-center gap-2">
<div className="flex flex-col gap-2 border-t px-4 py-3 shrink-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-h-7 items-center gap-2">
<FileUploadButton
onSelect={(file) => descEditorRef.current?.uploadFile(file)}
/>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<button
type="button"
onClick={switchToAgent}
title="Switch to create with agent — describe in one line and let the agent file it"
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
className="flex shrink-0 items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Switch to agent
Switch to Agent
</button>
<label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Switch
size="sm"
checked={keepOpen}
onCheckedChange={setKeepOpen}
/>
Create another
</label>
<Button size="sm" onClick={handleSubmit} disabled={!title.trim() || submitting}>
{submitting ? "Creating..." : "Create Issue"}
</Button>

View File

@@ -376,7 +376,7 @@ export function AgentCreatePanel({
className="flex shrink-0 items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Manual
Switch to Manual
</button>
<label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Switch