feat: enhance user display name resolution and update related components
This commit is contained in:
@@ -35,6 +35,11 @@ import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
|
||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||
import { createExponentialBackoff } from "@/lib/backoff";
|
||||
import {
|
||||
DEFAULT_HUMAN_LABEL,
|
||||
resolveHumanActorName,
|
||||
resolveMemberDisplayName,
|
||||
} from "@/lib/display-name";
|
||||
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePageActive } from "@/hooks/usePageActive";
|
||||
@@ -315,6 +320,11 @@ export default function ActivityPage() {
|
||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||
return member ? ["owner", "admin"].includes(member.role) : false;
|
||||
}, [membershipQuery.data]);
|
||||
const currentUserDisplayName = useMemo(() => {
|
||||
const member =
|
||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||
return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL);
|
||||
}, [membershipQuery.data]);
|
||||
|
||||
const [isFeedLoading, setIsFeedLoading] = useState(false);
|
||||
const [feedError, setFeedError] = useState<string | null>(null);
|
||||
@@ -344,7 +354,10 @@ export default function ActivityPage() {
|
||||
}, []);
|
||||
|
||||
const resolveAuthor = useCallback(
|
||||
(agentId: string | null | undefined, fallbackName = "Admin") => {
|
||||
(
|
||||
agentId: string | null | undefined,
|
||||
fallbackName: string = currentUserDisplayName,
|
||||
) => {
|
||||
if (agentId) {
|
||||
const agent = agentsByIdRef.current.get(agentId);
|
||||
if (agent) {
|
||||
@@ -361,7 +374,7 @@ export default function ActivityPage() {
|
||||
role: null,
|
||||
};
|
||||
},
|
||||
[],
|
||||
[currentUserDisplayName],
|
||||
);
|
||||
|
||||
const boardNameForId = useCallback((boardId: string | null | undefined) => {
|
||||
@@ -390,7 +403,7 @@ export default function ActivityPage() {
|
||||
? taskMetaByIdRef.current.get(event.task_id)
|
||||
: null;
|
||||
const boardId = meta?.boardId ?? null;
|
||||
const author = resolveAuthor(event.agent_id, "Admin");
|
||||
const author = resolveAuthor(event.agent_id, currentUserDisplayName);
|
||||
return {
|
||||
id: `activity:${event.id}`,
|
||||
created_at: event.created_at,
|
||||
@@ -407,7 +420,7 @@ export default function ActivityPage() {
|
||||
meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"),
|
||||
};
|
||||
},
|
||||
[boardNameForId, resolveAuthor],
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
);
|
||||
|
||||
const mapTaskComment = useCallback(
|
||||
@@ -416,7 +429,7 @@ export default function ActivityPage() {
|
||||
? taskMetaByIdRef.current.get(comment.task_id)
|
||||
: null;
|
||||
const boardId = meta?.boardId ?? fallbackBoardId;
|
||||
const author = resolveAuthor(comment.agent_id, "Admin");
|
||||
const author = resolveAuthor(comment.agent_id, currentUserDisplayName);
|
||||
return {
|
||||
id: `comment:${comment.id}`,
|
||||
created_at: comment.created_at,
|
||||
@@ -433,7 +446,7 @@ export default function ActivityPage() {
|
||||
meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"),
|
||||
};
|
||||
},
|
||||
[boardNameForId, resolveAuthor],
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
);
|
||||
|
||||
const mapApprovalEvent = useCallback(
|
||||
@@ -464,7 +477,7 @@ export default function ActivityPage() {
|
||||
? approval.created_at
|
||||
: (approval.resolved_at ?? approval.created_at);
|
||||
const action = humanizeApprovalAction(approval.action_type);
|
||||
const author = resolveAuthor(approval.agent_id, "Admin");
|
||||
const author = resolveAuthor(approval.agent_id, currentUserDisplayName);
|
||||
const statusText =
|
||||
nextStatus === "approved"
|
||||
? "approved"
|
||||
@@ -499,13 +512,16 @@ export default function ActivityPage() {
|
||||
title: `Approval · ${action}`,
|
||||
};
|
||||
},
|
||||
[boardNameForId, resolveAuthor],
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
);
|
||||
|
||||
const mapBoardChat = useCallback(
|
||||
(memory: BoardMemoryRead, boardId: string): FeedItem => {
|
||||
const content = (memory.content ?? "").trim();
|
||||
const actorName = (memory.source ?? "User").trim() || "User";
|
||||
const actorName = resolveHumanActorName(
|
||||
memory.source,
|
||||
currentUserDisplayName,
|
||||
);
|
||||
const command = content.startsWith("/");
|
||||
return {
|
||||
id: `chat:${memory.id}`,
|
||||
@@ -522,7 +538,7 @@ export default function ActivityPage() {
|
||||
title: command ? "Board command" : "Board chat",
|
||||
};
|
||||
},
|
||||
[boardNameForId],
|
||||
[boardNameForId, currentUserDisplayName],
|
||||
);
|
||||
|
||||
const mapAgentEvent = useCallback(
|
||||
|
||||
@@ -114,6 +114,11 @@ import {
|
||||
parseApiDatetime,
|
||||
toLocalDateInput,
|
||||
} from "@/lib/datetime";
|
||||
import {
|
||||
DEFAULT_HUMAN_LABEL,
|
||||
resolveHumanActorName,
|
||||
resolveMemberDisplayName,
|
||||
} from "@/lib/display-name";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePageActive } from "@/hooks/usePageActive";
|
||||
import {
|
||||
@@ -240,7 +245,7 @@ const toLiveFeedFromComment = (comment: TaskCommentRead): LiveFeedItem => ({
|
||||
|
||||
const toLiveFeedFromBoardChat = (memory: BoardChatMessage): LiveFeedItem => {
|
||||
const content = (memory.content ?? "").trim();
|
||||
const actorName = (memory.source ?? "User").trim() || "User";
|
||||
const actorName = resolveHumanActorName(memory.source, DEFAULT_HUMAN_LABEL);
|
||||
const isCommand = content.startsWith("/");
|
||||
return {
|
||||
id: `chat:${memory.id}`,
|
||||
@@ -591,15 +596,16 @@ TaskCommentCard.displayName = "TaskCommentCard";
|
||||
|
||||
const ChatMessageCard = memo(function ChatMessageCard({
|
||||
message,
|
||||
fallbackSource,
|
||||
}: {
|
||||
message: BoardChatMessage;
|
||||
fallbackSource: string;
|
||||
}) {
|
||||
const sourceLabel = resolveHumanActorName(message.source, fallbackSource);
|
||||
return (
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
{message.source ?? "User"}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-slate-900">{sourceLabel}</p>
|
||||
<span className="text-xs text-slate-400">
|
||||
{formatShortTimestamp(message.created_at)}
|
||||
</span>
|
||||
@@ -776,6 +782,11 @@ export default function BoardDetailPage() {
|
||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||
return member ? ["owner", "admin"].includes(member.role) : false;
|
||||
}, [membershipQuery.data]);
|
||||
const currentUserDisplayName = useMemo(() => {
|
||||
const member =
|
||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||
return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL);
|
||||
}, [membershipQuery.data]);
|
||||
const canWrite = boardAccess.canWrite;
|
||||
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
@@ -2002,6 +2013,7 @@ export default function BoardDetailPage() {
|
||||
{
|
||||
content: trimmed,
|
||||
tags: ["chat"],
|
||||
source: currentUserDisplayName,
|
||||
},
|
||||
);
|
||||
if (result.status !== 200) {
|
||||
@@ -2028,7 +2040,7 @@ export default function BoardDetailPage() {
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
},
|
||||
[boardId, isSignedIn, pushLiveFeed],
|
||||
[boardId, currentUserDisplayName, isSignedIn, pushLiveFeed],
|
||||
);
|
||||
|
||||
const handleSendChat = useCallback(
|
||||
@@ -3869,7 +3881,7 @@ export default function BoardDetailPage() {
|
||||
authorLabel={
|
||||
comment.agent_id
|
||||
? (assigneeById.get(comment.agent_id) ?? "Agent")
|
||||
: "Admin"
|
||||
: currentUserDisplayName
|
||||
}
|
||||
/>
|
||||
))}
|
||||
@@ -3918,7 +3930,11 @@ export default function BoardDetailPage() {
|
||||
</p>
|
||||
) : (
|
||||
chatMessages.map((message) => (
|
||||
<ChatMessageCard key={message.id} message={message} />
|
||||
<ChatMessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
fallbackSource={currentUserDisplayName}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div ref={chatEndRef} />
|
||||
@@ -3983,8 +3999,11 @@ export default function BoardDetailPage() {
|
||||
null)
|
||||
: null;
|
||||
const authorName =
|
||||
item.actor_name?.trim() ||
|
||||
(authorAgent ? authorAgent.name : "Admin");
|
||||
authorAgent?.name ??
|
||||
resolveHumanActorName(
|
||||
item.actor_name,
|
||||
currentUserDisplayName,
|
||||
);
|
||||
const authorRole = authorAgent
|
||||
? agentRoleLabel(authorAgent)
|
||||
: null;
|
||||
|
||||
56
frontend/src/lib/display-name.test.ts
Normal file
56
frontend/src/lib/display-name.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { OrganizationMemberRead } from "@/api/generated/model";
|
||||
import {
|
||||
DEFAULT_HUMAN_LABEL,
|
||||
normalizeDisplayName,
|
||||
resolveHumanActorName,
|
||||
resolveMemberDisplayName,
|
||||
} from "./display-name";
|
||||
|
||||
const memberWithUser = (user: {
|
||||
preferred_name?: string | null;
|
||||
name?: string | null;
|
||||
}): OrganizationMemberRead =>
|
||||
({
|
||||
user,
|
||||
}) as OrganizationMemberRead;
|
||||
|
||||
describe("display-name", () => {
|
||||
it("normalizes empty strings to null", () => {
|
||||
expect(normalizeDisplayName(" ")).toBeNull();
|
||||
expect(normalizeDisplayName(" Abhimanyu ")).toBe("Abhimanyu");
|
||||
});
|
||||
|
||||
it("resolves generic labels to fallback names", () => {
|
||||
expect(resolveHumanActorName("Admin", "Abhimanyu")).toBe("Abhimanyu");
|
||||
expect(resolveHumanActorName(" user ", "Abhimanyu")).toBe("Abhimanyu");
|
||||
});
|
||||
|
||||
it("keeps explicit non-generic actor labels", () => {
|
||||
expect(resolveHumanActorName("Abhimanyu", "User")).toBe("Abhimanyu");
|
||||
});
|
||||
|
||||
it("prefers membership preferred_name over name", () => {
|
||||
const member = memberWithUser({
|
||||
preferred_name: "Abhimanyu",
|
||||
name: "Admin",
|
||||
});
|
||||
expect(resolveMemberDisplayName(member)).toBe("Abhimanyu");
|
||||
});
|
||||
|
||||
it("falls back to membership name when preferred_name missing", () => {
|
||||
const member = memberWithUser({
|
||||
preferred_name: null,
|
||||
name: "Abhimanyu",
|
||||
});
|
||||
expect(resolveMemberDisplayName(member)).toBe("Abhimanyu");
|
||||
});
|
||||
|
||||
it("returns default user label when member names are unavailable", () => {
|
||||
expect(resolveMemberDisplayName(null)).toBe(DEFAULT_HUMAN_LABEL);
|
||||
expect(resolveMemberDisplayName(memberWithUser({ name: "Admin" }))).toBe(
|
||||
DEFAULT_HUMAN_LABEL,
|
||||
);
|
||||
});
|
||||
});
|
||||
33
frontend/src/lib/display-name.ts
Normal file
33
frontend/src/lib/display-name.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { OrganizationMemberRead } from "@/api/generated/model";
|
||||
|
||||
export const DEFAULT_HUMAN_LABEL = "User";
|
||||
|
||||
export const normalizeDisplayName = (
|
||||
value: string | null | undefined,
|
||||
): string | null => {
|
||||
const trimmed = (value ?? "").trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
};
|
||||
|
||||
export const resolveHumanActorName = (
|
||||
value: string | null | undefined,
|
||||
fallbackName: string = DEFAULT_HUMAN_LABEL,
|
||||
): string => {
|
||||
const normalized = normalizeDisplayName(value);
|
||||
if (!normalized) return fallbackName;
|
||||
const lowered = normalized.toLowerCase();
|
||||
if (lowered === "admin" || lowered === "user") {
|
||||
return fallbackName;
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const resolveMemberDisplayName = (
|
||||
member: OrganizationMemberRead | null | undefined,
|
||||
fallbackName: string = DEFAULT_HUMAN_LABEL,
|
||||
): string =>
|
||||
resolveHumanActorName(
|
||||
normalizeDisplayName(member?.user?.preferred_name) ??
|
||||
normalizeDisplayName(member?.user?.name),
|
||||
fallbackName,
|
||||
);
|
||||
Reference in New Issue
Block a user