Add me and unassigned assignee options
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -10,6 +10,7 @@ import { useCompany } from "../context/CompanyContext";
|
|||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { useProjectOrder } from "../hooks/useProjectOrder";
|
import { useProjectOrder } from "../hooks/useProjectOrder";
|
||||||
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
||||||
|
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||||
import { StatusIcon } from "./StatusIcon";
|
import { StatusIcon } from "./StatusIcon";
|
||||||
import { PriorityIcon } from "./PriorityIcon";
|
import { PriorityIcon } from "./PriorityIcon";
|
||||||
import { Identity } from "./Identity";
|
import { Identity } from "./Identity";
|
||||||
@@ -206,14 +207,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
const assignee = issue.assigneeAgentId
|
const assignee = issue.assigneeAgentId
|
||||||
? agents?.find((a) => a.id === issue.assigneeAgentId)
|
? agents?.find((a) => a.id === issue.assigneeAgentId)
|
||||||
: null;
|
: null;
|
||||||
const userLabel = (userId: string | null | undefined) =>
|
const userLabel = (userId: string | null | undefined) => formatAssigneeUserLabel(userId, currentUserId);
|
||||||
userId
|
|
||||||
? userId === "local-board"
|
|
||||||
? "Board"
|
|
||||||
: currentUserId && userId === currentUserId
|
|
||||||
? "Me"
|
|
||||||
: userId.slice(0, 5)
|
|
||||||
: null;
|
|
||||||
const assigneeUserLabel = userLabel(issue.assigneeUserId);
|
const assigneeUserLabel = userLabel(issue.assigneeUserId);
|
||||||
const creatorUserLabel = userLabel(issue.createdByUserId);
|
const creatorUserLabel = userLabel(issue.createdByUserId);
|
||||||
|
|
||||||
@@ -349,7 +343,22 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
>
|
>
|
||||||
No assignee
|
No assignee
|
||||||
</button>
|
</button>
|
||||||
{issue.createdByUserId && (
|
{currentUserId && (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||||
|
issue.assigneeUserId === currentUserId && "bg-accent",
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
onUpdate({ assigneeAgentId: null, assigneeUserId: currentUserId });
|
||||||
|
setAssigneeOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||||
|
Assign to me
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{issue.createdByUserId && issue.createdByUserId !== currentUserId && (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||||
@@ -361,7 +370,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
|
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||||
{creatorUserLabel ? `Assign to ${creatorUserLabel === "Me" ? "me" : creatorUserLabel}` : "Assign to requester"}
|
{creatorUserLabel ? `Assign to ${creatorUserLabel}` : "Assign to requester"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{sortedAgents
|
{sortedAgents
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { useDialog } from "../context/DialogContext";
|
import { useDialog } from "../context/DialogContext";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
|
import { authApi } from "../api/auth";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
|
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||||
import { groupBy } from "../lib/groupBy";
|
import { groupBy } from "../lib/groupBy";
|
||||||
import { formatDate, cn } from "../lib/utils";
|
import { formatDate, cn } from "../lib/utils";
|
||||||
import { timeAgo } from "../lib/timeAgo";
|
import { timeAgo } from "../lib/timeAgo";
|
||||||
@@ -87,11 +89,20 @@ function toggleInArray(arr: string[], value: string): string[] {
|
|||||||
return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value];
|
return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilters(issues: Issue[], state: IssueViewState): Issue[] {
|
function applyFilters(issues: Issue[], state: IssueViewState, currentUserId?: string | null): Issue[] {
|
||||||
let result = issues;
|
let result = issues;
|
||||||
if (state.statuses.length > 0) result = result.filter((i) => state.statuses.includes(i.status));
|
if (state.statuses.length > 0) result = result.filter((i) => state.statuses.includes(i.status));
|
||||||
if (state.priorities.length > 0) result = result.filter((i) => state.priorities.includes(i.priority));
|
if (state.priorities.length > 0) result = result.filter((i) => state.priorities.includes(i.priority));
|
||||||
if (state.assignees.length > 0) result = result.filter((i) => i.assigneeAgentId != null && state.assignees.includes(i.assigneeAgentId));
|
if (state.assignees.length > 0) {
|
||||||
|
result = result.filter((issue) => {
|
||||||
|
for (const assignee of state.assignees) {
|
||||||
|
if (assignee === "__unassigned" && !issue.assigneeAgentId && !issue.assigneeUserId) return true;
|
||||||
|
if (assignee === "__me" && currentUserId && issue.assigneeUserId === currentUserId) return true;
|
||||||
|
if (issue.assigneeAgentId === assignee) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (state.labels.length > 0) result = result.filter((i) => (i.labelIds ?? []).some((id) => state.labels.includes(id)));
|
if (state.labels.length > 0) result = result.filter((i) => (i.labelIds ?? []).some((id) => state.labels.includes(id)));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -165,6 +176,11 @@ export function IssuesList({
|
|||||||
}: IssuesListProps) {
|
}: IssuesListProps) {
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
const { openNewIssue } = useDialog();
|
const { openNewIssue } = useDialog();
|
||||||
|
const { data: session } = useQuery({
|
||||||
|
queryKey: queryKeys.auth.session,
|
||||||
|
queryFn: () => authApi.getSession(),
|
||||||
|
});
|
||||||
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
||||||
|
|
||||||
// Scope the storage key per company so folding/view state is independent across companies.
|
// Scope the storage key per company so folding/view state is independent across companies.
|
||||||
const scopedKey = selectedCompanyId ? `${viewStateKey}:${selectedCompanyId}` : viewStateKey;
|
const scopedKey = selectedCompanyId ? `${viewStateKey}:${selectedCompanyId}` : viewStateKey;
|
||||||
@@ -224,9 +240,9 @@ export function IssuesList({
|
|||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const sourceIssues = normalizedIssueSearch.length > 0 ? searchedIssues : issues;
|
const sourceIssues = normalizedIssueSearch.length > 0 ? searchedIssues : issues;
|
||||||
const filteredByControls = applyFilters(sourceIssues, viewState);
|
const filteredByControls = applyFilters(sourceIssues, viewState, currentUserId);
|
||||||
return sortIssues(filteredByControls, viewState);
|
return sortIssues(filteredByControls, viewState);
|
||||||
}, [issues, searchedIssues, viewState, normalizedIssueSearch]);
|
}, [issues, searchedIssues, viewState, normalizedIssueSearch, currentUserId]);
|
||||||
|
|
||||||
const { data: labels } = useQuery({
|
const { data: labels } = useQuery({
|
||||||
queryKey: queryKeys.issues.labels(selectedCompanyId!),
|
queryKey: queryKeys.issues.labels(selectedCompanyId!),
|
||||||
@@ -253,13 +269,21 @@ export function IssuesList({
|
|||||||
.map((p) => ({ key: p, label: statusLabel(p), items: groups[p]! }));
|
.map((p) => ({ key: p, label: statusLabel(p), items: groups[p]! }));
|
||||||
}
|
}
|
||||||
// assignee
|
// assignee
|
||||||
const groups = groupBy(filtered, (i) => i.assigneeAgentId ?? "__unassigned");
|
const groups = groupBy(
|
||||||
|
filtered,
|
||||||
|
(issue) => issue.assigneeAgentId ?? (issue.assigneeUserId ? `__user:${issue.assigneeUserId}` : "__unassigned"),
|
||||||
|
);
|
||||||
return Object.keys(groups).map((key) => ({
|
return Object.keys(groups).map((key) => ({
|
||||||
key,
|
key,
|
||||||
label: key === "__unassigned" ? "Unassigned" : (agentName(key) ?? key.slice(0, 8)),
|
label:
|
||||||
|
key === "__unassigned"
|
||||||
|
? "Unassigned"
|
||||||
|
: key.startsWith("__user:")
|
||||||
|
? (formatAssigneeUserLabel(key.slice("__user:".length), currentUserId) ?? "User")
|
||||||
|
: (agentName(key) ?? key.slice(0, 8)),
|
||||||
items: groups[key]!,
|
items: groups[key]!,
|
||||||
}));
|
}));
|
||||||
}, [filtered, viewState.groupBy, agents]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [filtered, viewState.groupBy, agents, agentName, currentUserId]);
|
||||||
|
|
||||||
const newIssueDefaults = (groupKey?: string) => {
|
const newIssueDefaults = (groupKey?: string) => {
|
||||||
const defaults: Record<string, string> = {};
|
const defaults: Record<string, string> = {};
|
||||||
@@ -267,13 +291,16 @@ export function IssuesList({
|
|||||||
if (groupKey) {
|
if (groupKey) {
|
||||||
if (viewState.groupBy === "status") defaults.status = groupKey;
|
if (viewState.groupBy === "status") defaults.status = groupKey;
|
||||||
else if (viewState.groupBy === "priority") defaults.priority = groupKey;
|
else if (viewState.groupBy === "priority") defaults.priority = groupKey;
|
||||||
else if (viewState.groupBy === "assignee" && groupKey !== "__unassigned") defaults.assigneeAgentId = groupKey;
|
else if (viewState.groupBy === "assignee" && groupKey !== "__unassigned") {
|
||||||
|
if (groupKey.startsWith("__user:")) defaults.assigneeUserId = groupKey.slice("__user:".length);
|
||||||
|
else defaults.assigneeAgentId = groupKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
const assignIssue = (issueId: string, assigneeAgentId: string | null) => {
|
const assignIssue = (issueId: string, assigneeAgentId: string | null, assigneeUserId: string | null = null) => {
|
||||||
onUpdateIssue(issueId, { assigneeAgentId, assigneeUserId: null });
|
onUpdateIssue(issueId, { assigneeAgentId, assigneeUserId });
|
||||||
setAssigneePickerIssueId(null);
|
setAssigneePickerIssueId(null);
|
||||||
setAssigneeSearch("");
|
setAssigneeSearch("");
|
||||||
};
|
};
|
||||||
@@ -419,11 +446,27 @@ export function IssuesList({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assignee */}
|
{/* Assignee */}
|
||||||
{agents && agents.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-muted-foreground">Assignee</span>
|
<span className="text-xs text-muted-foreground">Assignee</span>
|
||||||
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
||||||
{agents.map((agent) => (
|
<label className="flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-accent/50 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={viewState.assignees.includes("__unassigned")}
|
||||||
|
onCheckedChange={() => updateView({ assignees: toggleInArray(viewState.assignees, "__unassigned") })}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">No assignee</span>
|
||||||
|
</label>
|
||||||
|
{currentUserId && (
|
||||||
|
<label className="flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-accent/50 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={viewState.assignees.includes("__me")}
|
||||||
|
onCheckedChange={() => updateView({ assignees: toggleInArray(viewState.assignees, "__me") })}
|
||||||
|
/>
|
||||||
|
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-sm">Me</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{(agents ?? []).map((agent) => (
|
||||||
<label key={agent.id} className="flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-accent/50 cursor-pointer">
|
<label key={agent.id} className="flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-accent/50 cursor-pointer">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={viewState.assignees.includes(agent.id)}
|
checked={viewState.assignees.includes(agent.id)}
|
||||||
@@ -434,7 +477,6 @@ export function IssuesList({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{labels && labels.length > 0 && (
|
{labels && labels.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -683,6 +725,13 @@ export function IssuesList({
|
|||||||
>
|
>
|
||||||
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
|
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
|
||||||
<Identity name={agentName(issue.assigneeAgentId)!} size="sm" />
|
<Identity name={agentName(issue.assigneeAgentId)!} size="sm" />
|
||||||
|
) : issue.assigneeUserId ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs">
|
||||||
|
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-muted-foreground/35 bg-muted/30">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
{formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-muted-foreground/35 bg-muted/30">
|
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-muted-foreground/35 bg-muted/30">
|
||||||
@@ -701,7 +750,7 @@ export function IssuesList({
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
className="mb-1 w-full border-b border-border bg-transparent px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground/50"
|
className="mb-1 w-full border-b border-border bg-transparent px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground/50"
|
||||||
placeholder="Search agents..."
|
placeholder="Search assignees..."
|
||||||
value={assigneeSearch}
|
value={assigneeSearch}
|
||||||
onChange={(e) => setAssigneeSearch(e.target.value)}
|
onChange={(e) => setAssigneeSearch(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -710,16 +759,32 @@ export function IssuesList({
|
|||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-accent/50",
|
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-accent/50",
|
||||||
!issue.assigneeAgentId && "bg-accent",
|
!issue.assigneeAgentId && !issue.assigneeUserId && "bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
assignIssue(issue.id, null);
|
assignIssue(issue.id, null, null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No assignee
|
No assignee
|
||||||
</button>
|
</button>
|
||||||
|
{currentUserId && (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-accent/50",
|
||||||
|
issue.assigneeUserId === currentUserId && "bg-accent",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
assignIssue(issue.id, null, currentUserId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<User className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<span>Me</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{(agents ?? [])
|
{(agents ?? [])
|
||||||
.filter((agent) => {
|
.filter((agent) => {
|
||||||
if (!assigneeSearch.trim()) return true;
|
if (!assigneeSearch.trim()) return true;
|
||||||
@@ -737,7 +802,7 @@ export function IssuesList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
assignIssue(issue.id, agent.id);
|
assignIssue(issue.id, agent.id, null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Identity name={agent.name} size="sm" className="min-w-0" />
|
<Identity name={agent.name} size="sm" className="min-w-0" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface NewIssueDefaults {
|
|||||||
priority?: string;
|
priority?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
assigneeAgentId?: string;
|
assigneeAgentId?: string;
|
||||||
|
assigneeUserId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
53
ui/src/lib/assignees.test.ts
Normal file
53
ui/src/lib/assignees.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
assigneeValueFromSelection,
|
||||||
|
currentUserAssigneeOption,
|
||||||
|
formatAssigneeUserLabel,
|
||||||
|
parseAssigneeValue,
|
||||||
|
} from "./assignees";
|
||||||
|
|
||||||
|
describe("assignee selection helpers", () => {
|
||||||
|
it("encodes and parses agent assignees", () => {
|
||||||
|
const value = assigneeValueFromSelection({ assigneeAgentId: "agent-123" });
|
||||||
|
|
||||||
|
expect(value).toBe("agent:agent-123");
|
||||||
|
expect(parseAssigneeValue(value)).toEqual({
|
||||||
|
assigneeAgentId: "agent-123",
|
||||||
|
assigneeUserId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes and parses current-user assignees", () => {
|
||||||
|
const [option] = currentUserAssigneeOption("local-board");
|
||||||
|
|
||||||
|
expect(option).toEqual({
|
||||||
|
id: "user:local-board",
|
||||||
|
label: "Me",
|
||||||
|
searchText: "me board human local-board",
|
||||||
|
});
|
||||||
|
expect(parseAssigneeValue(option.id)).toEqual({
|
||||||
|
assigneeAgentId: null,
|
||||||
|
assigneeUserId: "local-board",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an empty selection as no assignee", () => {
|
||||||
|
expect(parseAssigneeValue("")).toEqual({
|
||||||
|
assigneeAgentId: null,
|
||||||
|
assigneeUserId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps backward compatibility for raw agent ids in saved drafts", () => {
|
||||||
|
expect(parseAssigneeValue("legacy-agent-id")).toEqual({
|
||||||
|
assigneeAgentId: "legacy-agent-id",
|
||||||
|
assigneeUserId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats current and board user labels consistently", () => {
|
||||||
|
expect(formatAssigneeUserLabel("user-1", "user-1")).toBe("Me");
|
||||||
|
expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board");
|
||||||
|
expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-");
|
||||||
|
});
|
||||||
|
});
|
||||||
51
ui/src/lib/assignees.ts
Normal file
51
ui/src/lib/assignees.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export interface AssigneeSelection {
|
||||||
|
assigneeAgentId: string | null;
|
||||||
|
assigneeUserId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssigneeOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
searchText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assigneeValueFromSelection(selection: Partial<AssigneeSelection>): string {
|
||||||
|
if (selection.assigneeAgentId) return `agent:${selection.assigneeAgentId}`;
|
||||||
|
if (selection.assigneeUserId) return `user:${selection.assigneeUserId}`;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAssigneeValue(value: string): AssigneeSelection {
|
||||||
|
if (!value) {
|
||||||
|
return { assigneeAgentId: null, assigneeUserId: null };
|
||||||
|
}
|
||||||
|
if (value.startsWith("agent:")) {
|
||||||
|
const assigneeAgentId = value.slice("agent:".length);
|
||||||
|
return { assigneeAgentId: assigneeAgentId || null, assigneeUserId: null };
|
||||||
|
}
|
||||||
|
if (value.startsWith("user:")) {
|
||||||
|
const assigneeUserId = value.slice("user:".length);
|
||||||
|
return { assigneeAgentId: null, assigneeUserId: assigneeUserId || null };
|
||||||
|
}
|
||||||
|
// Backward compatibility for older drafts/defaults that stored a raw agent id.
|
||||||
|
return { assigneeAgentId: value, assigneeUserId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentUserAssigneeOption(currentUserId: string | null | undefined): AssigneeOption[] {
|
||||||
|
if (!currentUserId) return [];
|
||||||
|
return [{
|
||||||
|
id: assigneeValueFromSelection({ assigneeUserId: currentUserId }),
|
||||||
|
label: "Me",
|
||||||
|
searchText: currentUserId === "local-board" ? "me board human local-board" : `me human ${currentUserId}`,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAssigneeUserLabel(
|
||||||
|
userId: string | null | undefined,
|
||||||
|
currentUserId: string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (!userId) return null;
|
||||||
|
if (currentUserId && userId === currentUserId) return "Me";
|
||||||
|
if (userId === "local-board") return "Board";
|
||||||
|
return userId.slice(0, 5);
|
||||||
|
}
|
||||||
@@ -304,8 +304,7 @@ export function IssueDetail() {
|
|||||||
options.push({ id: `agent:${agent.id}`, label: agent.name });
|
options.push({ id: `agent:${agent.id}`, label: agent.name });
|
||||||
}
|
}
|
||||||
if (currentUserId) {
|
if (currentUserId) {
|
||||||
const label = currentUserId === "local-board" ? "Board" : "Me (Board)";
|
options.push({ id: `user:${currentUserId}`, label: "Me" });
|
||||||
options.push({ id: `user:${currentUserId}`, label });
|
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}, [agents, currentUserId]);
|
}, [agents, currentUserId]);
|
||||||
|
|||||||
Reference in New Issue
Block a user