Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
b6da400a93 feat(create-issue): collapse start date into ⋯ overflow menu
Start date is a low-frequency field for most issues, so the always-on
inline pill was crowding the property toolbar. Move it behind the ⋯
overflow menu by default: the pill only appears once a value is set,
or transiently while the calendar popover is open after the user picks
"Set start date..." from the menu. Closing the popover without a value
returns the pill to the menu-only state.

To make the menu item open the popover programmatically, lift the
picker's open state via new controlled `open` / `onOpenChange` props
(matching the priority-picker pattern).

MUL-2557

Co-authored-by: multica-agent <github@multica.ai>
2026-05-22 15:04:55 +08:00
5 changed files with 67 additions and 10 deletions

View File

@@ -17,6 +17,8 @@ export function StartDatePicker({
onUpdate,
trigger: customTrigger,
triggerRender,
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
align = "start",
defaultOpen = false,
}: {
@@ -24,13 +26,17 @@ export function StartDatePicker({
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
trigger?: React.ReactNode;
triggerRender?: React.ReactElement;
open?: boolean;
onOpenChange?: (v: boolean) => void;
align?: "start" | "center" | "end";
/** Open the popover on first mount. Used by progressive-disclosure
* sidebars so a newly-added field immediately enters edit state. */
defaultOpen?: boolean;
}) {
const { t } = useT("issues");
const [open, setOpen] = useState(defaultOpen);
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const open = controlledOpen ?? internalOpen;
const setOpen = controlledOnOpenChange ?? setInternalOpen;
const date = startDate ? new Date(startDate) : undefined;
return (

View File

@@ -137,6 +137,7 @@
"subissue_of": "Sub-issue of {{identifier}}",
"subissue_chip": "Sub-issue: {{identifier}}",
"parent_with_id": "Parent: {{identifier}}",
"set_start_date": "Set start date...",
"set_parent": "Set parent issue...",
"add_subissue": "Add sub-issue...",
"remove_parent": "Remove parent",

View File

@@ -137,6 +137,7 @@
"subissue_of": "{{identifier}} 的子 issue",
"subissue_chip": "子 issue{{identifier}}",
"parent_with_id": "父:{{identifier}}",
"set_start_date": "设置开始日期...",
"set_parent": "设置父 issue...",
"add_subissue": "添加子 issue...",
"remove_parent": "移除父级",

View File

@@ -190,7 +190,15 @@ vi.mock("../issues/components", () => ({
StatusPicker: () => <div data-testid="status-picker" />,
PriorityPicker: () => <div data-testid="priority-picker" />,
AssigneePicker: () => <div data-testid="assignee-picker" />,
StartDatePicker: () => <div data-testid="start-date-picker" />,
// Surface open/onOpenChange so tests can assert progressive-disclosure
// behavior (mounted only when the user has opted in or has a value).
StartDatePicker: ({ open, onOpenChange }: { open?: boolean; onOpenChange?: (v: boolean) => void }) => (
<div
data-testid="start-date-picker"
data-open={open ? "true" : "false"}
onClick={() => onOpenChange?.(false)}
/>
),
DueDatePicker: () => <div data-testid="due-date-picker" />,
}));
@@ -556,6 +564,27 @@ describe("CreateIssueModal", () => {
);
});
// Start date is a low-frequency field — by default it lives behind the
// ⋯ overflow menu and is not rendered inline. Clicking the overflow
// entry opens it (and mounts the inline pill so the popover has an
// anchor); closing without picking returns it to the menu-only state.
it("hides start date behind the overflow menu and reveals it on demand", async () => {
const user = userEvent.setup();
renderModal(<CreateIssueModal onClose={vi.fn()} />);
expect(screen.queryByTestId("start-date-picker")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /Set start date/i }));
const picker = await screen.findByTestId("start-date-picker");
expect(picker).toHaveAttribute("data-open", "true");
await user.click(picker);
expect(screen.queryByTestId("start-date-picker")).not.toBeInTheDocument();
});
// Title + description are packed into the agent prompt on switch; if we
// leave them in the shared draft store, the next agent→manual switch
// surfaces the stale manual draft on top of the prompt-as-description,

View File

@@ -8,6 +8,7 @@ import {
ArrowDown,
ArrowLeftRight,
ArrowUp,
CalendarClock,
Check,
ChevronRight,
Maximize2,
@@ -128,6 +129,11 @@ export function ManualCreatePanel({
(data?.parent_issue_id as string) || undefined,
);
const [parentPickerOpen, setParentPickerOpen] = useState(false);
// Start date is a low-frequency field — by default it lives in the
// overflow ⋯ menu. Clicking the menu item flips this open, which both
// mounts the inline pill (the popover's anchor) AND opens the calendar.
// When the popover closes without a value set, the pill unmounts again.
const [startDatePickerOpen, setStartDatePickerOpen] = useState(false);
// Children live as full Issue objects — the picker always returns the whole
// object, and we never need to hydrate from an ID the way we do for parent.
const [childIssues, setChildIssues] = useState<Issue[]>([]);
@@ -494,14 +500,6 @@ export function ManualCreatePanel({
align="start"
/>
{/* Start date */}
<StartDatePicker
startDate={startDate}
onUpdate={(u) => updateStartDate(u.start_date ?? null)}
triggerRender={<PillButton />}
align="start"
/>
{/* Due date */}
<DueDatePicker
dueDate={dueDate}
@@ -518,6 +516,22 @@ export function ManualCreatePanel({
align="start"
/>
{/* Start date — collapsed into the ⋯ menu by default since it's
a low-frequency field. Renders inline only when the field
has a value OR the user just opened it from the overflow
menu (the picker's calendar popover needs the inline pill
as its anchor). */}
{(startDate || startDatePickerOpen) && (
<StartDatePicker
startDate={startDate}
onUpdate={(u) => updateStartDate(u.start_date ?? null)}
triggerRender={<PillButton />}
align="start"
open={startDatePickerOpen}
onOpenChange={setStartDatePickerOpen}
/>
)}
{/* Parent chip — appears when parent is set.
Placed before the ⋯ so it wraps to a new line with ⋯ if
space is tight, but ⋯ always stays last in DOM order. */}
@@ -579,6 +593,12 @@ export function ManualCreatePanel({
}
/>
<DropdownMenuContent align="start" className="w-auto">
{!startDate && (
<DropdownMenuItem onClick={() => setStartDatePickerOpen(true)}>
<CalendarClock className="h-3.5 w-3.5" />
{t(($) => $.create_issue.set_start_date)}
</DropdownMenuItem>
)}
{parentIssueId && parentIssue ? (
<DropdownMenuItem onClick={() => setParentPickerOpen(true)}>
<ArrowUp className="h-3.5 w-3.5" />