mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Resolve human user names and avatars in activity feeds
Activity feeds, issue detail, dashboard, chat thread, and live update toasts were showing raw user IDs (e.g. "FdpsZ") or generic "Board" for human users. Now all these surfaces resolve the user directory to display actual names and profile images. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
5c597993cd
commit
5ce75c3adf
8 files changed
+124
-34
No files matched your search
@@ -4,6 +4,7 @@ import { timeAgo } from "../lib/timeAgo";
|
||||
import { cn } from "../lib/utils";
|
||||
import { formatActivityVerb } from "../lib/activity-format";
|
||||
import { deriveProjectUrlKey, type ActivityEvent, type Agent } from "@paperclipai/shared";
|
||||
import type { CompanyUserProfile } from "../lib/company-members";
|
||||
|
||||
function entityLink(entityType: string, entityId: string, name?: string | null): string | null {
|
||||
switch (entityType) {
|
||||
@@ -19,13 +20,14 @@ function entityLink(entityType: string, entityId: string, name?: string | null):
|
||||
interface ActivityRowProps {
|
||||
event: ActivityEvent;
|
||||
agentMap: Map<string, Agent>;
|
||||
userProfileMap?: Map<string, CompanyUserProfile>;
|
||||
entityNameMap: Map<string, string>;
|
||||
entityTitleMap?: Map<string, string>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActivityRow({ event, agentMap, entityNameMap, entityTitleMap, className }: ActivityRowProps) {
|
||||
const verb = formatActivityVerb(event.action, event.details, { agentMap });
|
||||
export function ActivityRow({ event, agentMap, userProfileMap, entityNameMap, entityTitleMap, className }: ActivityRowProps) {
|
||||
const verb = formatActivityVerb(event.action, event.details, { agentMap, userProfileMap });
|
||||
|
||||
const isHeartbeatEvent = event.entityType === "heartbeat_run";
|
||||
const heartbeatAgentId = isHeartbeatEvent
|
||||
@@ -43,13 +45,16 @@ export function ActivityRow({ event, agentMap, entityNameMap, entityTitleMap, cl
|
||||
: entityLink(event.entityType, event.entityId, name);
|
||||
|
||||
const actor = event.actorType === "agent" ? agentMap.get(event.actorId) : null;
|
||||
const actorName = actor?.name ?? (event.actorType === "system" ? "System" : event.actorType === "user" ? "Board" : event.actorId || "Unknown");
|
||||
const userProfile = event.actorType === "user" ? userProfileMap?.get(event.actorId) : null;
|
||||
const actorName = actor?.name ?? (event.actorType === "system" ? "System" : userProfile?.label ?? (event.actorType === "user" ? "Board" : event.actorId || "Unknown"));
|
||||
const actorAvatarUrl = userProfile?.image ?? null;
|
||||
|
||||
const inner = (
|
||||
<div className="flex gap-3">
|
||||
<p className="flex-1 min-w-0 truncate">
|
||||
<Identity
|
||||
name={actorName}
|
||||
avatarUrl={actorAvatarUrl}
|
||||
size="xs"
|
||||
className="align-baseline"
|
||||
/>
|
||||
|
||||
@@ -88,6 +88,7 @@ interface IssueChatMessageContext {
|
||||
feedbackTermsUrl: string | null;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
userLabelMap?: ReadonlyMap<string, string> | null;
|
||||
onVote?: (
|
||||
commentId: string,
|
||||
vote: FeedbackVoteValue,
|
||||
@@ -192,6 +193,7 @@ interface IssueChatThreadProps {
|
||||
issueStatus?: string;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
userLabelMap?: ReadonlyMap<string, string> | null;
|
||||
onVote?: (
|
||||
commentId: string,
|
||||
vote: FeedbackVoteValue,
|
||||
@@ -444,12 +446,13 @@ function formatTimelineAssigneeLabel(
|
||||
assignee: IssueTimelineAssignee,
|
||||
agentMap?: Map<string, Agent>,
|
||||
currentUserId?: string | null,
|
||||
userLabelMap?: ReadonlyMap<string, string> | null,
|
||||
) {
|
||||
if (assignee.agentId) {
|
||||
return agentMap?.get(assignee.agentId)?.name ?? assignee.agentId.slice(0, 8);
|
||||
}
|
||||
if (assignee.userId) {
|
||||
return formatAssigneeUserLabel(assignee.userId, currentUserId) ?? "Board";
|
||||
return formatAssigneeUserLabel(assignee.userId, currentUserId, userLabelMap) ?? "Board";
|
||||
}
|
||||
return "Unassigned";
|
||||
}
|
||||
@@ -1402,7 +1405,7 @@ function IssueChatFeedbackButtons({
|
||||
}
|
||||
|
||||
function IssueChatSystemMessage() {
|
||||
const { agentMap, currentUserId } = useContext(IssueChatCtx);
|
||||
const { agentMap, currentUserId, userLabelMap } = useContext(IssueChatCtx);
|
||||
const message = useMessage();
|
||||
const custom = message.metadata.custom as Record<string, unknown>;
|
||||
const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined;
|
||||
@@ -1458,11 +1461,11 @@ function IssueChatSystemMessage() {
|
||||
Assignee
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{formatTimelineAssigneeLabel(assigneeChange.from, agentMap, currentUserId)}
|
||||
{formatTimelineAssigneeLabel(assigneeChange.from, agentMap, currentUserId, userLabelMap)}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-medium text-foreground">
|
||||
{formatTimelineAssigneeLabel(assigneeChange.to, agentMap, currentUserId)}
|
||||
{formatTimelineAssigneeLabel(assigneeChange.to, agentMap, currentUserId, userLabelMap)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1778,6 +1781,7 @@ export function IssueChatThread({
|
||||
issueStatus,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
onVote,
|
||||
onAdd,
|
||||
onCancelRun,
|
||||
@@ -1856,6 +1860,7 @@ export function IssueChatThread({
|
||||
projectId,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
}),
|
||||
[
|
||||
comments,
|
||||
@@ -1870,6 +1875,7 @@ export function IssueChatThread({
|
||||
projectId,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1912,6 +1918,7 @@ export function IssueChatThread({
|
||||
feedbackTermsUrl,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
onVote,
|
||||
onInterruptQueued,
|
||||
interruptingQueuedRunId,
|
||||
@@ -1923,6 +1930,7 @@ export function IssueChatThread({
|
||||
feedbackTermsUrl,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
onVote,
|
||||
onInterruptQueued,
|
||||
interruptingQueuedRunId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-quer
|
||||
import type { Agent, Issue, LiveEvent } from "@paperclipai/shared";
|
||||
import type { RunForIssue } from "../api/activity";
|
||||
import type { ActiveRunForIssue, LiveRunForIssue } from "../api/heartbeats";
|
||||
import type { CompanyUserDirectoryResponse } from "../api/access";
|
||||
import { authApi } from "../api/auth";
|
||||
import { useCompany } from "./CompanyContext";
|
||||
import type { ToastInput } from "./ToastContext";
|
||||
@@ -50,6 +51,19 @@ function resolveAgentName(
|
||||
return agent?.name ?? null;
|
||||
}
|
||||
|
||||
function resolveUserName(
|
||||
queryClient: QueryClient,
|
||||
companyId: string,
|
||||
userId: string,
|
||||
): string | null {
|
||||
const directory = queryClient.getQueryData<CompanyUserDirectoryResponse>(
|
||||
queryKeys.access.companyUserDirectory(companyId),
|
||||
);
|
||||
if (!directory) return null;
|
||||
const entry = directory.users.find((u) => u.principalId === userId);
|
||||
return entry?.user?.name?.trim() || entry?.user?.email?.trim() || null;
|
||||
}
|
||||
|
||||
function truncate(text: string, max: number): string {
|
||||
if (text.length <= max) return text;
|
||||
return text.slice(0, max - 1) + "\u2026";
|
||||
@@ -66,7 +80,7 @@ function resolveActorLabel(
|
||||
}
|
||||
if (actorType === "system") return "System";
|
||||
if (actorType === "user" && actorId) {
|
||||
return "Board";
|
||||
return resolveUserName(queryClient, companyId, actorId) ?? "Board";
|
||||
}
|
||||
return "Someone";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import type { CompanyUserProfile } from "./company-members";
|
||||
|
||||
type ActivityDetails = Record<string, unknown> | null | undefined;
|
||||
|
||||
@@ -16,6 +17,7 @@ type ActivityIssueReference = {
|
||||
|
||||
interface ActivityFormatOptions {
|
||||
agentMap?: Map<string, Agent>;
|
||||
userProfileMap?: Map<string, CompanyUserProfile>;
|
||||
currentUserId?: string | null;
|
||||
}
|
||||
|
||||
@@ -116,9 +118,11 @@ function readIssueReferences(details: ActivityDetails, key: string): ActivityIss
|
||||
return value.filter(isActivityIssueReference);
|
||||
}
|
||||
|
||||
function formatUserLabel(userId: string | null | undefined, currentUserId?: string | null): string {
|
||||
function formatUserLabel(userId: string | null | undefined, options: ActivityFormatOptions = {}): string {
|
||||
if (!userId || userId === "local-board") return "Board";
|
||||
if (currentUserId && userId === currentUserId) return "You";
|
||||
if (options.currentUserId && userId === options.currentUserId) return "You";
|
||||
const profile = options.userProfileMap?.get(userId);
|
||||
if (profile) return profile.label;
|
||||
return `user ${userId.slice(0, 5)}`;
|
||||
}
|
||||
|
||||
@@ -127,7 +131,7 @@ function formatParticipantLabel(participant: ActivityParticipant, options: Activ
|
||||
const agentId = participant.agentId ?? "";
|
||||
return options.agentMap?.get(agentId)?.name ?? "agent";
|
||||
}
|
||||
return formatUserLabel(participant.userId, options.currentUserId);
|
||||
return formatUserLabel(participant.userId, options);
|
||||
}
|
||||
|
||||
function formatIssueReferenceLabel(reference: ActivityIssueReference): string {
|
||||
@@ -165,7 +169,20 @@ function formatIssueUpdatedVerb(details: ActivityDetails): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatIssueUpdatedAction(details: ActivityDetails): string | null {
|
||||
function formatAssigneeName(details: ActivityDetails, options: ActivityFormatOptions): string | null {
|
||||
if (!details) return null;
|
||||
const agentId = details.assigneeAgentId;
|
||||
const userId = details.assigneeUserId;
|
||||
if (typeof agentId === "string" && agentId) {
|
||||
return options.agentMap?.get(agentId)?.name ?? "agent";
|
||||
}
|
||||
if (typeof userId === "string" && userId) {
|
||||
return formatUserLabel(userId, options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatIssueUpdatedAction(details: ActivityDetails, options: ActivityFormatOptions = {}): string | null {
|
||||
if (!details) return null;
|
||||
const previous = asRecord(details._previous) ?? {};
|
||||
const parts: string[] = [];
|
||||
@@ -187,7 +204,8 @@ function formatIssueUpdatedAction(details: ActivityDetails): string | null {
|
||||
);
|
||||
}
|
||||
if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) {
|
||||
parts.push(details.assigneeAgentId || details.assigneeUserId ? "assigned the issue" : "unassigned the issue");
|
||||
const assigneeName = formatAssigneeName(details, options);
|
||||
parts.push(assigneeName ? `assigned the issue to ${assigneeName}` : "unassigned the issue");
|
||||
}
|
||||
if (details.title !== undefined) parts.push("updated the title");
|
||||
if (details.description !== undefined) parts.push("updated the description");
|
||||
@@ -264,7 +282,7 @@ export function formatIssueActivityAction(
|
||||
options: ActivityFormatOptions = {},
|
||||
): string {
|
||||
if (action === "issue.updated") {
|
||||
const issueUpdatedAction = formatIssueUpdatedAction(details);
|
||||
const issueUpdatedAction = formatIssueUpdatedAction(details, options);
|
||||
if (issueUpdatedAction) return issueUpdatedAction;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,11 +230,12 @@ function authorNameForComment(
|
||||
comment: IssueChatComment,
|
||||
agentMap?: Map<string, Agent>,
|
||||
currentUserId?: string | null,
|
||||
userLabelMap?: ReadonlyMap<string, string> | null,
|
||||
) {
|
||||
if (comment.authorAgentId) {
|
||||
return agentMap?.get(comment.authorAgentId)?.name ?? comment.authorAgentId.slice(0, 8);
|
||||
}
|
||||
return formatAssigneeUserLabel(comment.authorUserId ?? null, currentUserId) ?? "You";
|
||||
return formatAssigneeUserLabel(comment.authorUserId ?? null, currentUserId, userLabelMap) ?? "You";
|
||||
}
|
||||
|
||||
function formatStatusLabel(status: string) {
|
||||
@@ -245,12 +246,13 @@ function createCommentMessage(args: {
|
||||
comment: IssueChatComment;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
userLabelMap?: ReadonlyMap<string, string> | null;
|
||||
companyId?: string | null;
|
||||
projectId?: string | null;
|
||||
}): ThreadMessage {
|
||||
const { comment, agentMap, currentUserId, companyId, projectId } = args;
|
||||
const { comment, agentMap, currentUserId, userLabelMap, companyId, projectId } = args;
|
||||
const createdAt = toDate(comment.createdAt);
|
||||
const authorName = authorNameForComment(comment, agentMap, currentUserId);
|
||||
const authorName = authorNameForComment(comment, agentMap, currentUserId, userLabelMap);
|
||||
const custom = {
|
||||
kind: "comment",
|
||||
commentId: comment.id,
|
||||
@@ -295,13 +297,14 @@ function createTimelineEventMessage(args: {
|
||||
event: IssueTimelineEvent;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
userLabelMap?: ReadonlyMap<string, string> | null;
|
||||
}) {
|
||||
const { event, agentMap, currentUserId } = args;
|
||||
const { event, agentMap, currentUserId, userLabelMap } = args;
|
||||
const actorName = event.actorType === "agent"
|
||||
? (agentMap?.get(event.actorId)?.name ?? event.actorId.slice(0, 8))
|
||||
: event.actorType === "system"
|
||||
? "System"
|
||||
: (formatAssigneeUserLabel(event.actorId, currentUserId) ?? "Board");
|
||||
: (formatAssigneeUserLabel(event.actorId, currentUserId, userLabelMap) ?? "Board");
|
||||
|
||||
const lines: string[] = [`${actorName} updated this issue`];
|
||||
if (event.statusChange) {
|
||||
@@ -312,10 +315,10 @@ function createTimelineEventMessage(args: {
|
||||
if (event.assigneeChange) {
|
||||
const from = event.assigneeChange.from.agentId
|
||||
? (agentMap?.get(event.assigneeChange.from.agentId)?.name ?? event.assigneeChange.from.agentId.slice(0, 8))
|
||||
: (formatAssigneeUserLabel(event.assigneeChange.from.userId, currentUserId) ?? "Unassigned");
|
||||
: (formatAssigneeUserLabel(event.assigneeChange.from.userId, currentUserId, userLabelMap) ?? "Unassigned");
|
||||
const to = event.assigneeChange.to.agentId
|
||||
? (agentMap?.get(event.assigneeChange.to.agentId)?.name ?? event.assigneeChange.to.agentId.slice(0, 8))
|
||||
: (formatAssigneeUserLabel(event.assigneeChange.to.userId, currentUserId) ?? "Unassigned");
|
||||
: (formatAssigneeUserLabel(event.assigneeChange.to.userId, currentUserId, userLabelMap) ?? "Unassigned");
|
||||
lines.push(`Assignee: ${from} -> ${to}`);
|
||||
}
|
||||
|
||||
@@ -703,6 +706,7 @@ export function buildIssueChatMessages(args: {
|
||||
projectId?: string | null;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
userLabelMap?: ReadonlyMap<string, string> | null;
|
||||
}) {
|
||||
const {
|
||||
comments,
|
||||
@@ -718,6 +722,7 @@ export function buildIssueChatMessages(args: {
|
||||
projectId,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
} = args;
|
||||
|
||||
const orderedMessages: MessageWithOrder[] = [];
|
||||
@@ -726,7 +731,7 @@ export function buildIssueChatMessages(args: {
|
||||
orderedMessages.push({
|
||||
createdAtMs: toTimestamp(comment.createdAt),
|
||||
order: 1,
|
||||
message: createCommentMessage({ comment, agentMap, currentUserId, companyId, projectId }),
|
||||
message: createCommentMessage({ comment, agentMap, currentUserId, userLabelMap, companyId, projectId }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -734,7 +739,7 @@ export function buildIssueChatMessages(args: {
|
||||
orderedMessages.push({
|
||||
createdAtMs: toTimestamp(event.createdAt),
|
||||
order: 0,
|
||||
message: createTimelineEventMessage({ event, agentMap, currentUserId }),
|
||||
message: createTimelineEventMessage({ event, agentMap, currentUserId, userLabelMap }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { accessApi } from "../api/access";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { buildCompanyUserProfileMap } from "../lib/company-members";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
@@ -60,6 +62,17 @@ export function Activity() {
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const userProfileMap = useMemo(
|
||||
() => buildCompanyUserProfileMap(companyMembers?.users),
|
||||
[companyMembers?.users],
|
||||
);
|
||||
|
||||
const agentMap = useMemo(() => {
|
||||
const map = new Map<string, Agent>();
|
||||
for (const a of agents ?? []) map.set(a.id, a);
|
||||
@@ -129,6 +142,7 @@ export function Activity() {
|
||||
key={event.id}
|
||||
event={event}
|
||||
agentMap={agentMap}
|
||||
userProfileMap={userProfileMap}
|
||||
entityNameMap={entityNameMap}
|
||||
entityTitleMap={entityTitleMap}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,12 @@ import { Link } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { dashboardApi } from "../api/dashboard";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { accessApi } from "../api/access";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { buildCompanyUserProfileMap } from "../lib/company-members";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
@@ -80,6 +82,17 @@ export function Dashboard() {
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const userProfileMap = useMemo(
|
||||
() => buildCompanyUserProfileMap(companyMembers?.users),
|
||||
[companyMembers?.users],
|
||||
);
|
||||
|
||||
const recentIssues = issues ? getRecentIssues(issues) : [];
|
||||
const recentActivity = useMemo(() => (activity ?? []).slice(0, 10), [activity]);
|
||||
|
||||
@@ -318,6 +331,7 @@ export function Dashboard() {
|
||||
key={event.id}
|
||||
event={event}
|
||||
agentMap={agentMap}
|
||||
userProfileMap={userProfileMap}
|
||||
entityNameMap={entityNameMap}
|
||||
entityTitleMap={entityTitleMap}
|
||||
className={animatedActivityIds.has(event.id) ? "activity-row-enter" : undefined}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { usePanel } from "../context/PanelContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees";
|
||||
import { buildCompanyUserInlineOptions, buildMarkdownMentionOptions } from "../lib/company-members";
|
||||
import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap, buildCompanyUserProfileMap, buildMarkdownMentionOptions } from "../lib/company-members";
|
||||
import { extractIssueTimelineEvents } from "../lib/issue-timeline-events";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import {
|
||||
@@ -222,14 +222,17 @@ function mergeOptimisticFeedbackVote(
|
||||
];
|
||||
}
|
||||
|
||||
function ActorIdentity({ evt, agentMap }: { evt: ActivityEvent; agentMap: Map<string, Agent> }) {
|
||||
function ActorIdentity({ evt, agentMap, userProfileMap }: { evt: ActivityEvent; agentMap: Map<string, Agent>; userProfileMap?: Map<string, import("../lib/company-members").CompanyUserProfile> }) {
|
||||
const id = evt.actorId;
|
||||
if (evt.actorType === "agent") {
|
||||
const agent = agentMap.get(id);
|
||||
return <Identity name={agent?.name ?? id.slice(0, 8)} size="sm" />;
|
||||
}
|
||||
if (evt.actorType === "system") return <Identity name="System" size="sm" />;
|
||||
if (evt.actorType === "user") return <Identity name="Board" size="sm" />;
|
||||
if (evt.actorType === "user") {
|
||||
const profile = userProfileMap?.get(id);
|
||||
return <Identity name={profile?.label ?? "Board"} avatarUrl={profile?.image} size="sm" />;
|
||||
}
|
||||
return <Identity name={id || "Unknown"} size="sm" />;
|
||||
}
|
||||
|
||||
@@ -534,8 +537,8 @@ export function IssueDetail() {
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyMembers(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listMembers(selectedCompanyId!),
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
@@ -589,6 +592,14 @@ export function IssueDetail() {
|
||||
for (const a of agents ?? []) map.set(a.id, a);
|
||||
return map;
|
||||
}, [agents]);
|
||||
const userProfileMap = useMemo(
|
||||
() => buildCompanyUserProfileMap(companyMembers?.users),
|
||||
[companyMembers?.users],
|
||||
);
|
||||
const userLabelMap = useMemo(
|
||||
() => buildCompanyUserLabelMap(companyMembers?.users),
|
||||
[companyMembers?.users],
|
||||
);
|
||||
const transcriptRuns = useMemo(
|
||||
() =>
|
||||
resolveIssueChatTranscriptRuns({
|
||||
@@ -611,9 +622,9 @@ export function IssueDetail() {
|
||||
return buildMarkdownMentionOptions({
|
||||
agents,
|
||||
projects: orderedProjects,
|
||||
members: companyMembers?.members,
|
||||
members: companyMembers?.users,
|
||||
});
|
||||
}, [agents, companyMembers?.members, orderedProjects]);
|
||||
}, [agents, companyMembers?.users, orderedProjects]);
|
||||
|
||||
const resolvedProject = useMemo(
|
||||
() => (issue?.projectId ? orderedProjects.find((project) => project.id === issue.projectId) ?? issue.project ?? null : null),
|
||||
@@ -665,7 +676,7 @@ export function IssueDetail() {
|
||||
|
||||
const commentReassignOptions = useMemo(() => {
|
||||
const options: Array<{ id: string; label: string; searchText?: string }> = [];
|
||||
options.push(...buildCompanyUserInlineOptions(companyMembers?.members, { excludeUserIds: [currentUserId] }));
|
||||
options.push(...buildCompanyUserInlineOptions(companyMembers?.users, { excludeUserIds: [currentUserId] }));
|
||||
const activeAgents = [...(agents ?? [])]
|
||||
.filter((agent) => agent.status !== "terminated")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -676,7 +687,7 @@ export function IssueDetail() {
|
||||
options.push({ id: `user:${currentUserId}`, label: "Me" });
|
||||
}
|
||||
return options;
|
||||
}, [agents, companyMembers?.members, currentUserId]);
|
||||
}, [agents, companyMembers?.users, currentUserId]);
|
||||
|
||||
const actualAssigneeValue = useMemo(
|
||||
() => assigneeValueFromSelection(issue ?? {}),
|
||||
@@ -2133,6 +2144,7 @@ export function IssueDetail() {
|
||||
issueStatus={issue.status}
|
||||
agentMap={agentMap}
|
||||
currentUserId={currentUserId}
|
||||
userLabelMap={userLabelMap}
|
||||
enableLiveTranscriptPolling={false}
|
||||
transcriptsByRunId={issueChatTranscriptByRun}
|
||||
hasOutputForRun={issueChatHasOutputForRun}
|
||||
@@ -2237,8 +2249,8 @@ export function IssueDetail() {
|
||||
<div className="space-y-1.5">
|
||||
{activity.slice(0, 20).map((evt) => (
|
||||
<div key={evt.id} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<ActorIdentity evt={evt} agentMap={agentMap} />
|
||||
<span>{formatIssueActivityAction(evt.action, evt.details, { agentMap, currentUserId })}</span>
|
||||
<ActorIdentity evt={evt} agentMap={agentMap} userProfileMap={userProfileMap} />
|
||||
<span>{formatIssueActivityAction(evt.action, evt.details, { agentMap, userProfileMap, currentUserId })}</span>
|
||||
<span className="ml-auto shrink-0">{relativeTime(evt.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user