mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
feat(views): progressive disclosure for issue sidebar properties (MUL-2275) (#2675)
* feat(views): progressive disclosure for issue sidebar properties (MUL-2275) Split sidebar Properties into a core group that always renders (status / priority / assignee / labels) and an optional group (due_date / project / parent) that only appears when the issue has the value set or the user explicitly added it via a new "+ Add property" picker. A field cleared in-session stays visible to avoid row flicker; navigating to a different issue reseeds visibility from that issue's set fields. The standalone "Parent issue" card is folded into Properties as one of those optional rows. Adds `defaultOpen` to DueDatePicker / ProjectPicker so a newly-added row drops the user straight into edit state. Co-authored-by: multica-agent <github@multica.ai> * refactor(views): swap sidebar optional set to due_date + labels Per design feedback: status / priority / assignee / project / parent are all required and should always render in the sidebar; only due_date and labels are progressive-disclosure optionals. Move project and parent rows out of the optional block (drop their +Add property menu entries and the parent special-case in addOptionalProp). Move labels into the optional block, gated on the issue's actual attached- label count (queried via issueLabelsOptions), with defaultOpen wired through LabelPicker so picking "Labels" from +Add property drops the user straight into the picker. Tests updated for the new split. Co-authored-by: multica-agent <github@multica.ai> * refactor(views): restore standalone parent card, move priority to optional Parent goes back to its own collapsible section, rendered only when the issue actually has a parent — matching the pre-MUL-2275 behavior. It is no longer interleaved with Properties rows. Priority joins the progressive-disclosure set (priority / due_date / labels). New issues default to priority "none", so the row is hidden until set or added via "+ Add property", and PriorityPicker gains defaultOpen so the field drops straight into edit state when chosen from the add-property menu. Co-authored-by: multica-agent <github@multica.ai> * refactor(issue-detail): tighten Add-property popover visual rhythm Picked up a small visual inconsistency while reviewing the PR's UI: the "Add property" dropdown floated above the inspector at a noticeably larger type scale than the property rows, and each item was bare text while the rows it sat above all rendered with an icon + value pair. Tweaks: - Items: `text-sm py-1.5` → `text-xs py-1`, matching the inspector row typography and trimming row-to-row gap from 12px to 8px. - Each option leads with the icon the resulting picker uses (`PriorityIcon` bars / `CalendarDays` / `Tag`) so the dropdown reads as a preview of what will appear in the new PropRow. - Focus indicator: replace the default thick focus ring with `focus-visible:bg-accent + outline-none`, matching the hover state language — keyboard focus and mouse hover now look the same. - Popover width: `w-48` → `w-44` since the labels are short and the visual is now denser; still leaves room for translated strings. * fix(issue-detail): dismiss Add-property popover when an option is picked Base UI's `Popover` doesn't auto-dismiss when a child is clicked (it's not a Menu primitive), so picking an option left the "+ Add property" popover sitting behind the picker that auto-opens for the newly added row — two popovers visibly stacked. Make the Popover controlled with a local `addPropPopoverOpen` state and close it inside `addOptionalProp` right after enqueuing the row's auto-open. The picker still pops on mount via `defaultOpen={autoOpenProp === key}`, so the user flow is unchanged from their perspective: Click "+ Add property" → menu opens Click an option → menu closes AND target picker opens (Was the same flow on paper before; just had the orphan popover behind the picker.) --------- Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -542,17 +542,54 @@ describe("IssueDetail (shared)", () => {
|
||||
expect(wsLink.closest("a")).toHaveAttribute("href", "/test/issues");
|
||||
});
|
||||
|
||||
it("renders properties sidebar with status, priority, assignee, due date", async () => {
|
||||
it("renders properties sidebar with all core rows plus set optional rows", async () => {
|
||||
renderIssueDetail();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Properties")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Core rows — always rendered regardless of whether the issue has a value.
|
||||
expect(screen.getByText("Status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Priority")).toBeInTheDocument();
|
||||
expect(screen.getByText("Assignee")).toBeInTheDocument();
|
||||
// "Project" appears twice (row label + picker stub), so disambiguate by id.
|
||||
expect(screen.getByTestId("project-picker")).toBeInTheDocument();
|
||||
// priority="high" + due_date are set in the fixture, so both optional rows show.
|
||||
expect(screen.getByText("Priority")).toBeInTheDocument();
|
||||
expect(screen.getByText("Due date")).toBeInTheDocument();
|
||||
// No labels are attached in the fixture — the Labels optional row
|
||||
// must stay hidden by default.
|
||||
expect(screen.queryByText("Labels")).not.toBeInTheDocument();
|
||||
// Parent issue lives in its own section and only renders when the
|
||||
// issue actually has a parent — the fixture has none.
|
||||
expect(screen.queryByText("Parent issue")).not.toBeInTheDocument();
|
||||
// The "+ Add property" affordance is always offered while any
|
||||
// optional field is still hidden.
|
||||
expect(screen.getByText("Add property")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides every optional property row when none are set", async () => {
|
||||
// Override the default fixture: nothing optional set.
|
||||
mockApiObj.getIssue.mockResolvedValue({
|
||||
...mockIssue,
|
||||
priority: "none",
|
||||
due_date: null,
|
||||
});
|
||||
|
||||
renderIssueDetail();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Properties")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText("Priority")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Due date")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Labels")).not.toBeInTheDocument();
|
||||
// Project stays as a core row regardless of value.
|
||||
expect(screen.getByTestId("project-picker")).toBeInTheDocument();
|
||||
// No parent → no standalone Parent issue section either.
|
||||
expect(screen.queryByText("Parent issue")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Add property")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses a non-resizable layout with the sidebar sheet closed by default on mobile", async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNavigation } from "../../navigation";
|
||||
import {
|
||||
Archive,
|
||||
Calendar,
|
||||
CalendarDays,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
Pin,
|
||||
PinOff,
|
||||
Plus,
|
||||
Tag,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { PageHeader } from "../../layout/page-header";
|
||||
@@ -58,6 +60,7 @@ import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths";
|
||||
import { useActorName } from "@multica/core/workspace/hooks";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { issueListOptions, issueDetailOptions, childIssuesOptions, issueUsageOptions, issueAttachmentsOptions } from "@multica/core/issues/queries";
|
||||
import { issueLabelsOptions } from "@multica/core/labels";
|
||||
import { memberListOptions, agentListOptions } from "@multica/core/workspace/queries";
|
||||
import { useRecentIssuesStore } from "@multica/core/issues/stores";
|
||||
import { useIssueSelectionStore } from "@multica/core/issues/stores/selection-store";
|
||||
@@ -265,6 +268,41 @@ function formatTokenCount(n: number): string {
|
||||
// new array on every render and bust React.memo on CommentCard / ResolvedThreadBar.
|
||||
const EMPTY_REPLIES: TimelineEntry[] = [];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar progressive disclosure
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Properties shown in the sidebar split into two groups:
|
||||
// - core: always rendered (status / assignee / project)
|
||||
// - optional: rendered only when the issue has a value for that field OR
|
||||
// the user explicitly added it via "+ Add property" in this session
|
||||
// (priority / due_date / labels)
|
||||
//
|
||||
// Parent is not in either group — it has its own standalone section below
|
||||
// the Properties block, rendered only when the issue actually has a parent.
|
||||
//
|
||||
// `OPTIONAL_PROP_KEYS` is the open set — adding a new optional field
|
||||
// (e.g. `start_date`) means appending here, wiring its row in the JSX
|
||||
// switch below, and adding a locale key. The picker, visibility rules,
|
||||
// and add-property menu all flow from this one list.
|
||||
const OPTIONAL_PROP_KEYS = ["priority", "due_date", "labels"] as const;
|
||||
type OptionalPropKey = (typeof OPTIONAL_PROP_KEYS)[number];
|
||||
|
||||
function isOptionalPropSet(
|
||||
issue: Issue,
|
||||
key: OptionalPropKey,
|
||||
attachedLabelsCount: number,
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case "priority":
|
||||
return issue.priority !== "none";
|
||||
case "due_date":
|
||||
return !!issue.due_date;
|
||||
case "labels":
|
||||
return attachedLabelsCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Shallow array equality by element identity. Used to reuse the previous
|
||||
// render's per-thread reply slice when nothing in *this* thread changed,
|
||||
// even if the surrounding `timeline` array was rebuilt by a WS event in
|
||||
@@ -593,6 +631,24 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
const [parentIssueOpen, setParentIssueOpen] = useState(true);
|
||||
const [pullRequestsOpen, setPullRequestsOpen] = useState(true);
|
||||
const [tokenUsageOpen, setTokenUsageOpen] = useState(true);
|
||||
|
||||
// Per-issue, per-session set of optional properties currently visible in
|
||||
// the sidebar Properties section. Seeded on issue switch with whichever
|
||||
// fields are already set; "+ Add property" adds an entry, clearing a
|
||||
// value does *not* remove one (avoids row-flicker on edit → clear).
|
||||
// Resets when the user navigates to a different issue.
|
||||
const [visibleOptionalProps, setVisibleOptionalProps] = useState<Set<OptionalPropKey>>(
|
||||
() => new Set(),
|
||||
);
|
||||
// Optional property to auto-open as soon as it's mounted (the user just
|
||||
// picked it from "+ Add property" and we want them dropped straight into
|
||||
// edit state). Consumed by the row that matches this key, cleared after.
|
||||
const [autoOpenProp, setAutoOpenProp] = useState<OptionalPropKey | null>(null);
|
||||
// Controlled state for the "+ Add property" popover. Base UI's Popover
|
||||
// doesn't auto-dismiss on item click (it's not a Menu primitive), so the
|
||||
// popover would stay open behind the newly auto-opened picker — two
|
||||
// popovers stacked. We close it explicitly in `addOptionalProp`.
|
||||
const [addPropPopoverOpen, setAddPropPopoverOpen] = useState(false);
|
||||
// Virtuoso's `customScrollParent` wants the HTMLElement, not a ref. A plain
|
||||
// `useRef.current` does not trigger a re-render when it populates, so the
|
||||
// Virtuoso prop would never receive the element. Callback ref + state fixes
|
||||
@@ -1002,6 +1058,66 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
const actions = useIssueActions(issue);
|
||||
const handleUpdateField = actions.updateField;
|
||||
|
||||
// Labels live in their own query (not on the issue body) — fetch the count
|
||||
// here so seeding can decide whether the "Labels" optional row should be
|
||||
// shown for an issue that already has labels attached.
|
||||
const { data: attachedLabels = [] } = useQuery(issueLabelsOptions(wsId, id));
|
||||
const attachedLabelsCount = attachedLabels.length;
|
||||
|
||||
// Seed the visible-optional-props set:
|
||||
// - on issue switch, reset to whichever fields are currently set
|
||||
// - on the SAME issue, additively pick up fields the user just set
|
||||
// (so the row stays visible after they edit + clear in one session)
|
||||
// Removal happens only on issue switch — never on clear.
|
||||
const seededIssueIdRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!issue) return;
|
||||
if (seededIssueIdRef.current !== issue.id) {
|
||||
seededIssueIdRef.current = issue.id;
|
||||
setAutoOpenProp(null);
|
||||
const seed = new Set<OptionalPropKey>();
|
||||
for (const k of OPTIONAL_PROP_KEYS) {
|
||||
if (isOptionalPropSet(issue, k, attachedLabelsCount)) seed.add(k);
|
||||
}
|
||||
setVisibleOptionalProps(seed);
|
||||
return;
|
||||
}
|
||||
setVisibleOptionalProps((prev) => {
|
||||
let next = prev;
|
||||
for (const k of OPTIONAL_PROP_KEYS) {
|
||||
if (isOptionalPropSet(issue, k, attachedLabelsCount) && !next.has(k)) {
|
||||
if (next === prev) next = new Set(prev);
|
||||
next.add(k);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [issue, attachedLabelsCount]);
|
||||
|
||||
const addOptionalProp = useCallback(
|
||||
(key: OptionalPropKey) => {
|
||||
setVisibleOptionalProps((prev) => {
|
||||
if (prev.has(key)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(key);
|
||||
return next;
|
||||
});
|
||||
setAutoOpenProp(key);
|
||||
// Dismiss the "+ Add property" popover so it doesn't sit stacked
|
||||
// behind the picker we're about to auto-open.
|
||||
setAddPropPopoverOpen(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Clear the auto-open flag after the next render so pickers (which read
|
||||
// `defaultOpen` once via a useState initializer) keep the open state they
|
||||
// captured on mount, but later interactions don't re-trigger it.
|
||||
useEffect(() => {
|
||||
if (autoOpenProp === null) return;
|
||||
setAutoOpenProp(null);
|
||||
}, [autoOpenProp]);
|
||||
|
||||
const handleToggleSidebar = useCallback(() => {
|
||||
if (isMobile) {
|
||||
setMobileSidebarOpen((open) => !open);
|
||||
@@ -1090,28 +1206,104 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
<ChevronRight className={`!size-3 shrink-0 stroke-[2.5] text-muted-foreground transition-transform ${propertiesOpen ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
{propertiesOpen && <div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5 pl-2">
|
||||
{/* Core props — always rendered. */}
|
||||
<PropRow label={t(($) => $.detail.prop_status)}>
|
||||
<StatusPicker status={issue.status} onUpdate={handleUpdateField} align="start" />
|
||||
</PropRow>
|
||||
<PropRow label={t(($) => $.detail.prop_priority)}>
|
||||
<PriorityPicker priority={issue.priority} onUpdate={handleUpdateField} align="start" />
|
||||
</PropRow>
|
||||
<PropRow label={t(($) => $.detail.prop_assignee)}>
|
||||
<AssigneePicker assigneeType={issue.assignee_type} assigneeId={issue.assignee_id} onUpdate={handleUpdateField} align="start" />
|
||||
</PropRow>
|
||||
<PropRow label={t(($) => $.detail.prop_due_date)}>
|
||||
<DueDatePicker dueDate={issue.due_date} onUpdate={handleUpdateField} />
|
||||
</PropRow>
|
||||
<PropRow label={t(($) => $.detail.prop_project)}>
|
||||
<ProjectPicker projectId={issue.project_id} onUpdate={handleUpdateField} />
|
||||
</PropRow>
|
||||
<PropRow label={t(($) => $.detail.prop_labels)}>
|
||||
<LabelPicker issueId={issue.id} align="start" />
|
||||
<ProjectPicker
|
||||
projectId={issue.project_id}
|
||||
onUpdate={handleUpdateField}
|
||||
/>
|
||||
</PropRow>
|
||||
|
||||
{/* Optional props — rendered only when set on the issue OR added
|
||||
via "+ Add property" in this session. Row order follows the
|
||||
order of `OPTIONAL_PROP_KEYS`. */}
|
||||
{visibleOptionalProps.has("priority") && (
|
||||
<PropRow label={t(($) => $.detail.prop_priority)}>
|
||||
<PriorityPicker
|
||||
priority={issue.priority}
|
||||
onUpdate={handleUpdateField}
|
||||
align="start"
|
||||
defaultOpen={autoOpenProp === "priority"}
|
||||
/>
|
||||
</PropRow>
|
||||
)}
|
||||
{visibleOptionalProps.has("due_date") && (
|
||||
<PropRow label={t(($) => $.detail.prop_due_date)}>
|
||||
<DueDatePicker
|
||||
dueDate={issue.due_date}
|
||||
onUpdate={handleUpdateField}
|
||||
defaultOpen={autoOpenProp === "due_date"}
|
||||
/>
|
||||
</PropRow>
|
||||
)}
|
||||
{visibleOptionalProps.has("labels") && (
|
||||
<PropRow label={t(($) => $.detail.prop_labels)}>
|
||||
<LabelPicker
|
||||
issueId={issue.id}
|
||||
align="start"
|
||||
defaultOpen={autoOpenProp === "labels"}
|
||||
/>
|
||||
</PropRow>
|
||||
)}
|
||||
|
||||
{/* "+ Add property" — opens a Popover listing optional fields
|
||||
not yet displayed. Hidden once every optional field is on
|
||||
screen. Sits inside the same grid as a full-row, with its
|
||||
own padding so the visual rhythm follows the rows above. */}
|
||||
{OPTIONAL_PROP_KEYS.some((k) => !visibleOptionalProps.has(k)) && (
|
||||
<div className="col-span-2 mt-1">
|
||||
<Popover open={addPropPopoverOpen} onOpenChange={setAddPropPopoverOpen}>
|
||||
<PopoverTrigger
|
||||
className="flex items-center gap-1.5 rounded-md px-2 py-1 -mx-2 text-xs text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
|
||||
>
|
||||
<Plus className="h-3 w-3 shrink-0" />
|
||||
<span>{t(($) => $.detail.add_property_action)}</span>
|
||||
</PopoverTrigger>
|
||||
{/* Item visuals mirror the inspector rows' typography
|
||||
(text-xs, muted icons) and each option leads with the
|
||||
icon the resulting picker uses, so the dropdown reads
|
||||
as a preview of what will show up below. */}
|
||||
<PopoverContent align="start" className="w-44 p-1">
|
||||
{OPTIONAL_PROP_KEYS.filter((k) => !visibleOptionalProps.has(k)).map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
onClick={() => addOptionalProp(k)}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-foreground/90 transition-colors hover:bg-accent focus-visible:bg-accent focus-visible:outline-none"
|
||||
>
|
||||
{k === "priority" && (
|
||||
<PriorityIcon priority="medium" inheritColor className="text-muted-foreground" />
|
||||
)}
|
||||
{k === "due_date" && (
|
||||
<CalendarDays className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
{k === "labels" && (
|
||||
<Tag className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate">
|
||||
{k === "priority" && t(($) => $.detail.prop_priority)}
|
||||
{k === "due_date" && t(($) => $.detail.prop_due_date)}
|
||||
{k === "labels" && t(($) => $.detail.prop_labels)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* Parent issue */}
|
||||
{/* Parent issue — standalone section, only when the issue has a
|
||||
parent. Setting a parent is reachable via the issue actions menu;
|
||||
this card surfaces an existing parent without occupying sidebar
|
||||
space for issues that don't have one. */}
|
||||
{parentIssue && (
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -18,15 +18,19 @@ export function DueDatePicker({
|
||||
trigger: customTrigger,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
dueDate: string | null;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
trigger?: React.ReactNode;
|
||||
triggerRender?: React.ReactElement;
|
||||
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(false);
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const date = dueDate ? new Date(dueDate) : undefined;
|
||||
const isOverdue = date ? date < new Date() : false;
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ interface LabelPickerProps {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
align?: "start" | "center" | "end";
|
||||
/** Open the picker on first mount. Used by progressive-disclosure
|
||||
* sidebars so a newly-added field immediately enters edit state. */
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,9 +70,10 @@ export function LabelPicker({
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
align = "start",
|
||||
defaultOpen = false,
|
||||
}: LabelPickerProps) {
|
||||
const { t } = useT("issues");
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = onOpenChange ?? setInternalOpen;
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
@@ -15,6 +15,7 @@ export function PriorityPicker({
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
align,
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
priority: IssuePriority;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
@@ -23,8 +24,11 @@ export function PriorityPicker({
|
||||
open?: boolean;
|
||||
onOpenChange?: (v: boolean) => void;
|
||||
align?: "start" | "center" | "end";
|
||||
/** Open the picker on first mount. Used by progressive-disclosure
|
||||
* sidebars so a newly-added field immediately enters edit state. */
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
const { t } = useT("issues");
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"prop_due_date": "Due date",
|
||||
"prop_project": "Project",
|
||||
"prop_labels": "Labels",
|
||||
"add_property_action": "Add property",
|
||||
"prop_created_by": "Created by",
|
||||
"prop_created": "Created",
|
||||
"prop_updated": "Updated",
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"prop_due_date": "截止日期",
|
||||
"prop_project": "项目",
|
||||
"prop_labels": "标签",
|
||||
"add_property_action": "添加字段",
|
||||
"prop_created_by": "创建者",
|
||||
"prop_created": "创建时间",
|
||||
"prop_updated": "更新时间",
|
||||
|
||||
@@ -20,11 +20,15 @@ export function ProjectPicker({
|
||||
onUpdate,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
projectId: string | null;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
triggerRender?: React.ReactElement;
|
||||
align?: "start" | "center" | "end";
|
||||
/** Open the dropdown on first mount. Used by progressive-disclosure
|
||||
* sidebars so a newly-added field immediately enters edit state. */
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const { t } = useT("projects");
|
||||
const wsId = useWorkspaceId();
|
||||
@@ -32,7 +36,7 @@ export function ProjectPicker({
|
||||
const current = projects.find((p) => p.id === projectId);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu defaultOpen={defaultOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={triggerRender ? undefined : "flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden"}
|
||||
render={triggerRender}
|
||||
|
||||
Reference in New Issue
Block a user