Move reviewer/approver to rows under assignee with three-dot menu

- Comment out non-functional Labels chip in new-issue bottom bar
- Remove reviewer/approver mini pills from bottom chip bar
- Add three-dot menu (⋯) next to Project selector in the "For/in" row
- Clicking Reviewer or Approver in that menu toggles a full-sized
  participant selector row under Assignee, matching its styling
- Toggling off clears the selection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-06 19:59:17 -05:00
parent 71d93c79a5
commit e7fe02c02f

View file

@ -288,6 +288,9 @@ export function NewIssueDialog() {
const [assigneeValue, setAssigneeValue] = useState("");
const [reviewerValue, setReviewerValue] = useState("");
const [approverValue, setApproverValue] = useState("");
const [showReviewerRow, setShowReviewerRow] = useState(false);
const [showApproverRow, setShowApproverRow] = useState(false);
const [participantMenuOpen, setParticipantMenuOpen] = useState(false);
const [projectId, setProjectId] = useState("");
const [projectWorkspaceId, setProjectWorkspaceId] = useState("");
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
@ -560,6 +563,8 @@ export function NewIssueDialog() {
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setReviewerValue("");
setApproverValue("");
setShowReviewerRow(false);
setShowApproverRow(false);
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
@ -580,6 +585,8 @@ export function NewIssueDialog() {
);
setReviewerValue(draft.reviewerValue ?? "");
setApproverValue(draft.approverValue ?? "");
setShowReviewerRow(!!(draft.reviewerValue));
setShowApproverRow(!!(draft.approverValue));
setProjectId(restoredProjectId);
setProjectWorkspaceId(draft.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(restoredProject));
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
@ -601,6 +608,8 @@ export function NewIssueDialog() {
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setReviewerValue("");
setApproverValue("");
setShowReviewerRow(false);
setShowApproverRow(false);
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
@ -645,6 +654,8 @@ export function NewIssueDialog() {
setAssigneeValue("");
setReviewerValue("");
setApproverValue("");
setShowReviewerRow(false);
setShowApproverRow(false);
setProjectId("");
setProjectWorkspaceId("");
setAssigneeOptionsOpen(false);
@ -668,6 +679,8 @@ export function NewIssueDialog() {
setAssigneeValue("");
setReviewerValue("");
setApproverValue("");
setShowReviewerRow(false);
setShowApproverRow(false);
setProjectId("");
setProjectWorkspaceId("");
setAssigneeModelOverride("");
@ -1179,8 +1192,139 @@ export function NewIssueDialog() {
);
}}
/>
{/* Three-dot menu to add Reviewer / Approver rows */}
<Popover open={participantMenuOpen} onOpenChange={setParticipantMenuOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="inline-flex items-center justify-center rounded-md p-1 text-muted-foreground hover:bg-accent/50 transition-colors"
title="Add reviewer or approver"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</PopoverTrigger>
<PopoverContent className="w-44 p-1" align="start">
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
showReviewerRow && "bg-accent",
)}
onClick={() => {
setShowReviewerRow((v) => !v);
if (showReviewerRow) setReviewerValue("");
setParticipantMenuOpen(false);
}}
>
<Eye className="h-3 w-3" />
Reviewer
</button>
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
showApproverRow && "bg-accent",
)}
onClick={() => {
setShowApproverRow((v) => !v);
if (showApproverRow) setApproverValue("");
setParticipantMenuOpen(false);
}}
>
<ShieldCheck className="h-3 w-3" />
Approver
</button>
</PopoverContent>
</Popover>
</div>
</div>
{/* Reviewer row */}
{showReviewerRow && (
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
<Eye className="h-3.5 w-3.5 shrink-0" />
<InlineEntitySelector
value={reviewerValue}
options={assigneeOptions}
placeholder="Reviewer"
disablePortal
noneLabel="No reviewer"
searchPlaceholder="Search reviewers..."
emptyMessage="No reviewers found."
onChange={setReviewerValue}
renderTriggerValue={(option) =>
option ? (
<>
{(() => {
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
})()}
<span className="truncate">{option.label}</span>
</>
) : (
<span className="text-muted-foreground">Reviewer</span>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return (
<>
{reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span>
</>
);
}}
/>
</div>
)}
{/* Approver row */}
{showApproverRow && (
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
<ShieldCheck className="h-3.5 w-3.5 shrink-0" />
<InlineEntitySelector
value={approverValue}
options={assigneeOptions}
placeholder="Approver"
disablePortal
noneLabel="No approver"
searchPlaceholder="Search approvers..."
emptyMessage="No approvers found."
onChange={setApproverValue}
renderTriggerValue={(option) =>
option ? (
<>
{(() => {
const approver = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
})()}
<span className="truncate">{option.label}</span>
</>
) : (
<span className="text-muted-foreground">Approver</span>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const approver = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return (
<>
{approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span>
</>
);
}}
/>
</div>
)}
</div>
{isSubIssueMode ? (
@ -1467,11 +1611,11 @@ export function NewIssueDialog() {
</PopoverContent>
</Popover>
{/* Labels chip (placeholder) */}
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
{/* Labels chip — disabled, not wired up yet */}
{/* <button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
<Tag className="h-3 w-3" />
Labels
</button>
</button> */}
<input
ref={stageFileInputRef}
@ -1490,80 +1634,6 @@ export function NewIssueDialog() {
Upload
</button>
<InlineEntitySelector
value={reviewerValue}
options={assigneeOptions}
placeholder="Reviewer"
disablePortal
noneLabel="No reviewer"
searchPlaceholder="Search reviewers..."
emptyMessage="No reviewers found."
onChange={setReviewerValue}
className="bg-transparent font-normal text-xs text-muted-foreground"
renderTriggerValue={(option) =>
option ? (
<>
<Eye className="h-3 w-3" />
<span className="truncate">{option.label}</span>
</>
) : (
<>
<Eye className="h-3 w-3" />
<span>Reviewer</span>
</>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return (
<>
{reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span>
</>
);
}}
/>
<InlineEntitySelector
value={approverValue}
options={assigneeOptions}
placeholder="Approver"
disablePortal
noneLabel="No approver"
searchPlaceholder="Search approvers..."
emptyMessage="No approvers found."
onChange={setApproverValue}
className="bg-transparent font-normal text-xs text-muted-foreground"
renderTriggerValue={(option) =>
option ? (
<>
<ShieldCheck className="h-3 w-3" />
<span className="truncate">{option.label}</span>
</>
) : (
<>
<ShieldCheck className="h-3 w-3" />
<span>Approver</span>
</>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const approver = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return (
<>
{approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span>
</>
);
}}
/>
{/* More (dates) */}
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
<PopoverTrigger asChild>