forked from farhoodlabs/paperclip
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user