mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 20:15:37 +02:00
refactor(projects): align create-project footer with create-issue
Restructure the create-project modal footer to match the create-issue pattern (per design feedback): the primary action moves out of the cramped single justify-between row into its own border-t action strip, and the property pills sit in a dedicated wrapping toolbar above it. Low-frequency fields (start/due date) collapse into a ⋯ overflow via progressive disclosure — a pill only renders inline once its date is set or the user opens it from the menu — so the default toolbar stays a clean single row (Status · Priority · Lead · Repos · ⋯). - Use the shared PillButton (../common/pill-button) instead of the modal-local copy, gaining the data-popup-open styling create-issue uses. - ProjectStartDatePicker / ProjectDueDatePicker gain controlled open props so the overflow menu can reveal + open them (mirrors the issue pickers). - i18n: create_project.set_start_date / set_due_date / more_options_aria across en / zh-Hans / ja / ko, reusing the create-issue wording. Test updated to assert the dates are revealed from the overflow rather than shown inline by default. typecheck / lint / i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -109,6 +109,9 @@
|
||||
"description_hint": "Shared with agents as context for every task in this project.",
|
||||
"lead": "Lead",
|
||||
"no_lead": "No lead",
|
||||
"set_start_date": "Set start date...",
|
||||
"set_due_date": "Set due date...",
|
||||
"more_options_aria": "More options",
|
||||
"lead_placeholder": "Assign lead...",
|
||||
"members_group": "Members",
|
||||
"agents_group": "Agents",
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
"description_hint": "このプロジェクトのすべてのタスクで、コンテキストとしてエージェントに共有されます。",
|
||||
"lead": "リード",
|
||||
"no_lead": "リードなし",
|
||||
"set_start_date": "開始日を設定...",
|
||||
"set_due_date": "期限を設定...",
|
||||
"more_options_aria": "その他のオプション",
|
||||
"lead_placeholder": "リードを指定...",
|
||||
"members_group": "メンバー",
|
||||
"agents_group": "エージェント",
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
"description_hint": "이 프로젝트의 모든 작업에서 컨텍스트로 에이전트에게 공유됩니다.",
|
||||
"lead": "리드",
|
||||
"no_lead": "리드 없음",
|
||||
"set_start_date": "시작일 설정...",
|
||||
"set_due_date": "마감일 설정...",
|
||||
"more_options_aria": "추가 옵션",
|
||||
"lead_placeholder": "리드 지정...",
|
||||
"members_group": "멤버",
|
||||
"agents_group": "에이전트",
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
"description_hint": "会作为项目上下文提供给智能体,应用于该项目下的每个任务。",
|
||||
"lead": "负责人",
|
||||
"no_lead": "无负责人",
|
||||
"set_start_date": "设置开始日期...",
|
||||
"set_due_date": "设置截止日期...",
|
||||
"more_options_aria": "更多选项",
|
||||
"lead_placeholder": "指派负责人...",
|
||||
"members_group": "成员",
|
||||
"agents_group": "智能体",
|
||||
|
||||
@@ -181,10 +181,18 @@ describe("CreateProjectModal", () => {
|
||||
expect(screen.getByRole("tooltip", { name: longRepoUrl })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the start-date and due-date pills", () => {
|
||||
it("reveals the start/due date pickers from the ⋯ overflow menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithI18n(<CreateProjectModal onClose={vi.fn()} />);
|
||||
|
||||
// Dates are collapsed behind the overflow by default (progressive disclosure).
|
||||
expect(screen.queryByRole("button", { name: "Start date" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Due date" })).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Set start date/ }));
|
||||
expect(screen.getByRole("button", { name: "Start date" })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Set due date/ }));
|
||||
expect(screen.getByRole("button", { name: "Due date" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { ChevronRight, FolderOpen, Maximize2, Minimize2, Search, X as XIcon, UserMinus } from "lucide-react";
|
||||
import { CalendarClock, CalendarDays, ChevronRight, FolderOpen, Maximize2, Minimize2, MoreHorizontal, Search, X as XIcon, UserMinus } from "lucide-react";
|
||||
|
||||
/**
|
||||
* GitHub mark — lucide-react v1 dropped brand icons, so we inline the
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
} from "../projects/components/labels";
|
||||
import { ProjectStartDatePicker } from "../projects/components/project-start-date-picker";
|
||||
import { ProjectDueDatePicker } from "../projects/components/project-due-date-picker";
|
||||
import { PillButton } from "../common/pill-button";
|
||||
import {
|
||||
isDesktopShell,
|
||||
pickDirectory,
|
||||
@@ -66,26 +67,6 @@ import {
|
||||
} from "../platform/local-directory";
|
||||
import { useLocalDaemonStatus } from "../platform/use-local-daemon-status";
|
||||
|
||||
function PillButton({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
|
||||
"hover:bg-accent/60 transition-colors cursor-pointer",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function RepoUrlText({
|
||||
url,
|
||||
className,
|
||||
@@ -138,6 +119,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
const [icon, setIcon] = useState<string | undefined>(draft.icon);
|
||||
const [startDate, setStartDate] = useState<string>(draft.startDate ?? "");
|
||||
const [dueDate, setDueDate] = useState<string>(draft.dueDate ?? "");
|
||||
// Dates are collapsed into the ⋯ overflow by default (progressive
|
||||
// disclosure, mirroring create-issue); these flip a pill inline + open.
|
||||
const [startDatePickerOpen, setStartDatePickerOpen] = useState(false);
|
||||
const [dueDatePickerOpen, setDueDatePickerOpen] = useState(false);
|
||||
const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
@@ -407,14 +392,13 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer: properties (left, wrap) + Create button (right). Single row
|
||||
so the modal stays compact — Linear-style.
|
||||
{/* Property toolbar — mirrors the create-issue footer: a wrapping pill
|
||||
row whose low-frequency fields (start/due date) collapse into a ⋯
|
||||
overflow, with the primary action in a separate bar below.
|
||||
Repos lives here alongside the property pills for now. Once we
|
||||
support more resource types (Linear / Notion / Figma / Slack), pull
|
||||
them out into a dedicated Resources strip above this footer — a
|
||||
single Repos pill on its own row looked too sparse. */}
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3 border-t shrink-0">
|
||||
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
||||
them out into a dedicated Resources strip above this footer. */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 shrink-0 flex-wrap">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
@@ -549,17 +533,27 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<ProjectStartDatePicker
|
||||
startDate={startDate || null}
|
||||
onUpdate={(u) => updateStartDate(u.start_date ?? "")}
|
||||
triggerRender={<PillButton />}
|
||||
/>
|
||||
{/* Start date — collapsed into ⋯ unless it has a value or was just
|
||||
opened from the overflow (the calendar anchors on the inline pill). */}
|
||||
{(startDate || startDatePickerOpen) && (
|
||||
<ProjectStartDatePicker
|
||||
startDate={startDate || null}
|
||||
onUpdate={(u) => updateStartDate(u.start_date ?? "")}
|
||||
triggerRender={<PillButton />}
|
||||
open={startDatePickerOpen}
|
||||
onOpenChange={setStartDatePickerOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProjectDueDatePicker
|
||||
dueDate={dueDate || null}
|
||||
onUpdate={(u) => updateDueDate(u.due_date ?? "")}
|
||||
triggerRender={<PillButton />}
|
||||
/>
|
||||
{(dueDate || dueDatePickerOpen) && (
|
||||
<ProjectDueDatePicker
|
||||
dueDate={dueDate || null}
|
||||
onUpdate={(u) => updateDueDate(u.due_date ?? "")}
|
||||
triggerRender={<PillButton />}
|
||||
open={dueDatePickerOpen}
|
||||
onOpenChange={setDueDatePickerOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
open={repoPopoverOpen}
|
||||
@@ -809,8 +803,40 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Overflow — always the last child so it stays at the end of the
|
||||
wrap flow. Only rendered while a date is still collapsible; when
|
||||
both are set there is nothing left to add. */}
|
||||
{(!startDate || !dueDate) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<PillButton aria-label={t(($) => $.create_project.more_options_aria)}>
|
||||
<MoreHorizontal className="size-3.5" />
|
||||
</PillButton>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-auto">
|
||||
{!dueDate && (
|
||||
<DropdownMenuItem onClick={() => setDueDatePickerOpen(true)}>
|
||||
<CalendarDays className="h-3.5 w-3.5" />
|
||||
{t(($) => $.create_project.set_due_date)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!startDate && (
|
||||
<DropdownMenuItem onClick={() => setStartDatePickerOpen(true)}>
|
||||
<CalendarClock className="h-3.5 w-3.5" />
|
||||
{t(($) => $.create_project.set_start_date)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer action bar — primary action in its own strip, matching
|
||||
create-issue. */}
|
||||
<div className="flex items-center justify-end border-t px-4 py-3 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -30,15 +30,22 @@ export function ProjectDueDatePicker({
|
||||
onUpdate,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
}: {
|
||||
dueDate: string | null;
|
||||
onUpdate: (updates: Partial<UpdateProjectRequest>) => void;
|
||||
/** Custom trigger element (e.g. a pill button in the create modal). */
|
||||
triggerRender?: React.ReactElement;
|
||||
align?: "start" | "center" | "end";
|
||||
/** Controlled open state — lets a ⋯ overflow menu reveal + open the pill. */
|
||||
open?: boolean;
|
||||
onOpenChange?: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useT("projects");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
const date = dateOnlyToLocalDate(dueDate);
|
||||
const isOverdue = isPastDateOnly(dueDate);
|
||||
|
||||
|
||||
@@ -31,15 +31,22 @@ export function ProjectStartDatePicker({
|
||||
onUpdate,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
}: {
|
||||
startDate: string | null;
|
||||
onUpdate: (updates: Partial<UpdateProjectRequest>) => void;
|
||||
/** Custom trigger element (e.g. a pill button in the create modal). */
|
||||
triggerRender?: React.ReactElement;
|
||||
align?: "start" | "center" | "end";
|
||||
/** Controlled open state — lets a ⋯ overflow menu reveal + open the pill. */
|
||||
open?: boolean;
|
||||
onOpenChange?: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useT("projects");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
const date = dateOnlyToLocalDate(startDate);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user