Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
c90e566d11 feat(create-issue): default assignee to last-selected value
The create-issue modal now remembers the assignee picked at submit
time and prefills the picker with that value when the modal next
opens. Implemented by tracking lastAssigneeType/Id alongside the
draft and seeding clearDraft's reset with those values.
2026-04-28 15:01:55 +08:00
4 changed files with 85 additions and 1 deletions

View File

@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useIssueDraftStore } from "./draft-store";
const RESET_STATE = {
draft: {
title: "",
description: "",
status: "todo" as const,
priority: "none" as const,
assigneeType: undefined,
assigneeId: undefined,
dueDate: null,
},
lastAssigneeType: undefined,
lastAssigneeId: undefined,
};
describe("issue draft store — last assignee", () => {
beforeEach(() => {
useIssueDraftStore.setState(RESET_STATE);
});
it("clearDraft prefills the next draft with the remembered assignee", () => {
const { setDraft, setLastAssignee, clearDraft } =
useIssueDraftStore.getState();
setDraft({ title: "first", assigneeType: "member", assigneeId: "alice" });
setLastAssignee("member", "alice");
clearDraft();
const { draft } = useIssueDraftStore.getState();
expect(draft.title).toBe("");
expect(draft.assigneeType).toBe("member");
expect(draft.assigneeId).toBe("alice");
});
it("clearDraft yields an empty assignee when none has ever been remembered", () => {
const { setDraft, clearDraft } = useIssueDraftStore.getState();
setDraft({ title: "first" });
clearDraft();
const { draft } = useIssueDraftStore.getState();
expect(draft.assigneeType).toBeUndefined();
expect(draft.assigneeId).toBeUndefined();
});
it("setLastAssignee(undefined) lets the user opt back out of a default", () => {
const { setLastAssignee, clearDraft } = useIssueDraftStore.getState();
setLastAssignee("member", "alice");
clearDraft();
expect(useIssueDraftStore.getState().draft.assigneeId).toBe("alice");
setLastAssignee(undefined, undefined);
clearDraft();
expect(useIssueDraftStore.getState().draft.assigneeId).toBeUndefined();
expect(useIssueDraftStore.getState().draft.assigneeType).toBeUndefined();
});
});

View File

@@ -26,8 +26,14 @@ const EMPTY_DRAFT: IssueDraft = {
interface IssueDraftStore {
draft: IssueDraft;
// Last assignee picked at submit time. Persisted across drafts so the
// create-issue modal can prefill the picker with the user's most recent
// choice instead of always opening with no assignee.
lastAssigneeType?: IssueAssigneeType;
lastAssigneeId?: string;
setDraft: (patch: Partial<IssueDraft>) => void;
clearDraft: () => void;
setLastAssignee: (type?: IssueAssigneeType, id?: string) => void;
hasDraft: () => boolean;
}
@@ -35,9 +41,20 @@ export const useIssueDraftStore = create<IssueDraftStore>()(
persist(
(set, get) => ({
draft: { ...EMPTY_DRAFT },
lastAssigneeType: undefined,
lastAssigneeId: undefined,
setDraft: (patch) =>
set((s) => ({ draft: { ...s.draft, ...patch } })),
clearDraft: () => set({ draft: { ...EMPTY_DRAFT } }),
clearDraft: () =>
set((s) => ({
draft: {
...EMPTY_DRAFT,
assigneeType: s.lastAssigneeType,
assigneeId: s.lastAssigneeId,
},
})),
setLastAssignee: (type, id) =>
set({ lastAssigneeType: type, lastAssigneeId: id }),
hasDraft: () => {
const { draft } = get();
return !!(draft.title || draft.description);

View File

@@ -8,6 +8,7 @@ const mockPush = vi.hoisted(() => vi.fn());
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 mockToastCustom = vi.hoisted(() => vi.fn());
const mockToastDismiss = vi.hoisted(() => vi.fn());
const mockToastError = vi.hoisted(() => vi.fn());
@@ -22,8 +23,11 @@ const mockDraftStore = {
assigneeId: undefined,
dueDate: null,
},
lastAssigneeType: undefined,
lastAssigneeId: undefined,
setDraft: mockSetDraft,
clearDraft: mockClearDraft,
setLastAssignee: mockSetLastAssignee,
};
vi.mock("../navigation", () => ({
@@ -238,6 +242,7 @@ describe("CreateIssueModal", () => {
});
});
expect(mockSetLastAssignee).toHaveBeenCalledWith(undefined, undefined);
expect(mockClearDraft).toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
expect(mockToastCustom).toHaveBeenCalledTimes(1);

View File

@@ -57,6 +57,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
const draft = useIssueDraftStore((s) => s.draft);
const setDraft = useIssueDraftStore((s) => s.setDraft);
const clearDraft = useIssueDraftStore((s) => s.clearDraft);
const setLastAssignee = useIssueDraftStore((s) => s.setLastAssignee);
const [title, setTitle] = useState(draft.title);
const descEditorRef = useRef<ContentEditorRef>(null);
@@ -153,6 +154,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
}
}
setLastAssignee(assigneeType, assigneeId);
clearDraft();
const shouldShowBacklogHint =
status === "backlog" && assigneeType === "agent" && assigneeId &&