mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 19:00:38 +09:00
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:
parent
71d93c79a5
commit
e7fe02c02f
1 changed files with 147 additions and 77 deletions
|
|
@ -288,6 +288,9 @@ export function NewIssueDialog() {
|
||||||
const [assigneeValue, setAssigneeValue] = useState("");
|
const [assigneeValue, setAssigneeValue] = useState("");
|
||||||
const [reviewerValue, setReviewerValue] = useState("");
|
const [reviewerValue, setReviewerValue] = useState("");
|
||||||
const [approverValue, setApproverValue] = 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 [projectId, setProjectId] = useState("");
|
||||||
const [projectWorkspaceId, setProjectWorkspaceId] = useState("");
|
const [projectWorkspaceId, setProjectWorkspaceId] = useState("");
|
||||||
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
|
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
|
||||||
|
|
@ -560,6 +563,8 @@ export function NewIssueDialog() {
|
||||||
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
||||||
setReviewerValue("");
|
setReviewerValue("");
|
||||||
setApproverValue("");
|
setApproverValue("");
|
||||||
|
setShowReviewerRow(false);
|
||||||
|
setShowApproverRow(false);
|
||||||
setAssigneeModelOverride("");
|
setAssigneeModelOverride("");
|
||||||
setAssigneeThinkingEffort("");
|
setAssigneeThinkingEffort("");
|
||||||
setAssigneeChrome(false);
|
setAssigneeChrome(false);
|
||||||
|
|
@ -580,6 +585,8 @@ export function NewIssueDialog() {
|
||||||
);
|
);
|
||||||
setReviewerValue(draft.reviewerValue ?? "");
|
setReviewerValue(draft.reviewerValue ?? "");
|
||||||
setApproverValue(draft.approverValue ?? "");
|
setApproverValue(draft.approverValue ?? "");
|
||||||
|
setShowReviewerRow(!!(draft.reviewerValue));
|
||||||
|
setShowApproverRow(!!(draft.approverValue));
|
||||||
setProjectId(restoredProjectId);
|
setProjectId(restoredProjectId);
|
||||||
setProjectWorkspaceId(draft.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(restoredProject));
|
setProjectWorkspaceId(draft.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(restoredProject));
|
||||||
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
|
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
|
||||||
|
|
@ -601,6 +608,8 @@ export function NewIssueDialog() {
|
||||||
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
||||||
setReviewerValue("");
|
setReviewerValue("");
|
||||||
setApproverValue("");
|
setApproverValue("");
|
||||||
|
setShowReviewerRow(false);
|
||||||
|
setShowApproverRow(false);
|
||||||
setAssigneeModelOverride("");
|
setAssigneeModelOverride("");
|
||||||
setAssigneeThinkingEffort("");
|
setAssigneeThinkingEffort("");
|
||||||
setAssigneeChrome(false);
|
setAssigneeChrome(false);
|
||||||
|
|
@ -645,6 +654,8 @@ export function NewIssueDialog() {
|
||||||
setAssigneeValue("");
|
setAssigneeValue("");
|
||||||
setReviewerValue("");
|
setReviewerValue("");
|
||||||
setApproverValue("");
|
setApproverValue("");
|
||||||
|
setShowReviewerRow(false);
|
||||||
|
setShowApproverRow(false);
|
||||||
setProjectId("");
|
setProjectId("");
|
||||||
setProjectWorkspaceId("");
|
setProjectWorkspaceId("");
|
||||||
setAssigneeOptionsOpen(false);
|
setAssigneeOptionsOpen(false);
|
||||||
|
|
@ -668,6 +679,8 @@ export function NewIssueDialog() {
|
||||||
setAssigneeValue("");
|
setAssigneeValue("");
|
||||||
setReviewerValue("");
|
setReviewerValue("");
|
||||||
setApproverValue("");
|
setApproverValue("");
|
||||||
|
setShowReviewerRow(false);
|
||||||
|
setShowApproverRow(false);
|
||||||
setProjectId("");
|
setProjectId("");
|
||||||
setProjectWorkspaceId("");
|
setProjectWorkspaceId("");
|
||||||
setAssigneeModelOverride("");
|
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>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{isSubIssueMode ? (
|
{isSubIssueMode ? (
|
||||||
|
|
@ -1467,11 +1611,11 @@ export function NewIssueDialog() {
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{/* Labels chip (placeholder) */}
|
{/* 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">
|
{/* <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" />
|
<Tag className="h-3 w-3" />
|
||||||
Labels
|
Labels
|
||||||
</button>
|
</button> */}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={stageFileInputRef}
|
ref={stageFileInputRef}
|
||||||
|
|
@ -1490,80 +1634,6 @@ export function NewIssueDialog() {
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</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) */}
|
{/* More (dates) */}
|
||||||
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue