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 { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||||
import { createExponentialBackoff } from "@/lib/backoff";
|
import { createExponentialBackoff } from "@/lib/backoff";
|
||||||
|
import {
|
||||||
|
DEFAULT_HUMAN_LABEL,
|
||||||
|
resolveHumanActorName,
|
||||||
|
resolveMemberDisplayName,
|
||||||
|
} from "@/lib/display-name";
|
||||||
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
|
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { usePageActive } from "@/hooks/usePageActive";
|
import { usePageActive } from "@/hooks/usePageActive";
|
||||||
@@ -315,6 +320,11 @@ export default function ActivityPage() {
|
|||||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||||
return member ? ["owner", "admin"].includes(member.role) : false;
|
return member ? ["owner", "admin"].includes(member.role) : false;
|
||||||
}, [membershipQuery.data]);
|
}, [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 [isFeedLoading, setIsFeedLoading] = useState(false);
|
||||||
const [feedError, setFeedError] = useState<string | null>(null);
|
const [feedError, setFeedError] = useState<string | null>(null);
|
||||||
@@ -344,7 +354,10 @@ export default function ActivityPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resolveAuthor = useCallback(
|
const resolveAuthor = useCallback(
|
||||||
(agentId: string | null | undefined, fallbackName = "Admin") => {
|
(
|
||||||
|
agentId: string | null | undefined,
|
||||||
|
fallbackName: string = currentUserDisplayName,
|
||||||
|
) => {
|
||||||
if (agentId) {
|
if (agentId) {
|
||||||
const agent = agentsByIdRef.current.get(agentId);
|
const agent = agentsByIdRef.current.get(agentId);
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -361,7 +374,7 @@ export default function ActivityPage() {
|
|||||||
role: null,
|
role: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[],
|
[currentUserDisplayName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const boardNameForId = useCallback((boardId: string | null | undefined) => {
|
const boardNameForId = useCallback((boardId: string | null | undefined) => {
|
||||||
@@ -390,7 +403,7 @@ export default function ActivityPage() {
|
|||||||
? taskMetaByIdRef.current.get(event.task_id)
|
? taskMetaByIdRef.current.get(event.task_id)
|
||||||
: null;
|
: null;
|
||||||
const boardId = meta?.boardId ?? null;
|
const boardId = meta?.boardId ?? null;
|
||||||
const author = resolveAuthor(event.agent_id, "Admin");
|
const author = resolveAuthor(event.agent_id, currentUserDisplayName);
|
||||||
return {
|
return {
|
||||||
id: `activity:${event.id}`,
|
id: `activity:${event.id}`,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
@@ -407,7 +420,7 @@ export default function ActivityPage() {
|
|||||||
meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"),
|
meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[boardNameForId, resolveAuthor],
|
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapTaskComment = useCallback(
|
const mapTaskComment = useCallback(
|
||||||
@@ -416,7 +429,7 @@ export default function ActivityPage() {
|
|||||||
? taskMetaByIdRef.current.get(comment.task_id)
|
? taskMetaByIdRef.current.get(comment.task_id)
|
||||||
: null;
|
: null;
|
||||||
const boardId = meta?.boardId ?? fallbackBoardId;
|
const boardId = meta?.boardId ?? fallbackBoardId;
|
||||||
const author = resolveAuthor(comment.agent_id, "Admin");
|
const author = resolveAuthor(comment.agent_id, currentUserDisplayName);
|
||||||
return {
|
return {
|
||||||
id: `comment:${comment.id}`,
|
id: `comment:${comment.id}`,
|
||||||
created_at: comment.created_at,
|
created_at: comment.created_at,
|
||||||
@@ -433,7 +446,7 @@ export default function ActivityPage() {
|
|||||||
meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"),
|
meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[boardNameForId, resolveAuthor],
|
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapApprovalEvent = useCallback(
|
const mapApprovalEvent = useCallback(
|
||||||
@@ -464,7 +477,7 @@ export default function ActivityPage() {
|
|||||||
? approval.created_at
|
? approval.created_at
|
||||||
: (approval.resolved_at ?? approval.created_at);
|
: (approval.resolved_at ?? approval.created_at);
|
||||||
const action = humanizeApprovalAction(approval.action_type);
|
const action = humanizeApprovalAction(approval.action_type);
|
||||||
const author = resolveAuthor(approval.agent_id, "Admin");
|
const author = resolveAuthor(approval.agent_id, currentUserDisplayName);
|
||||||
const statusText =
|
const statusText =
|
||||||
nextStatus === "approved"
|
nextStatus === "approved"
|
||||||
? "approved"
|
? "approved"
|
||||||
@@ -499,13 +512,16 @@ export default function ActivityPage() {
|
|||||||
title: `Approval · ${action}`,
|
title: `Approval · ${action}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[boardNameForId, resolveAuthor],
|
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapBoardChat = useCallback(
|
const mapBoardChat = useCallback(
|
||||||
(memory: BoardMemoryRead, boardId: string): FeedItem => {
|
(memory: BoardMemoryRead, boardId: string): FeedItem => {
|
||||||
const content = (memory.content ?? "").trim();
|
const content = (memory.content ?? "").trim();
|
||||||
const actorName = (memory.source ?? "User").trim() || "User";
|
const actorName = resolveHumanActorName(
|
||||||
|
memory.source,
|
||||||
|
currentUserDisplayName,
|
||||||
|
);
|
||||||
const command = content.startsWith("/");
|
const command = content.startsWith("/");
|
||||||
return {
|
return {
|
||||||
id: `chat:${memory.id}`,
|
id: `chat:${memory.id}`,
|
||||||
@@ -522,7 +538,7 @@ export default function ActivityPage() {
|
|||||||
title: command ? "Board command" : "Board chat",
|
title: command ? "Board command" : "Board chat",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[boardNameForId],
|
[boardNameForId, currentUserDisplayName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapAgentEvent = useCallback(
|
const mapAgentEvent = useCallback(
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ import {
|
|||||||
parseApiDatetime,
|
parseApiDatetime,
|
||||||
toLocalDateInput,
|
toLocalDateInput,
|
||||||
} from "@/lib/datetime";
|
} from "@/lib/datetime";
|
||||||
|
import {
|
||||||
|
DEFAULT_HUMAN_LABEL,
|
||||||
|
resolveHumanActorName,
|
||||||
|
resolveMemberDisplayName,
|
||||||
|
} from "@/lib/display-name";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { usePageActive } from "@/hooks/usePageActive";
|
import { usePageActive } from "@/hooks/usePageActive";
|
||||||
import {
|
import {
|
||||||
@@ -240,7 +245,7 @@ const toLiveFeedFromComment = (comment: TaskCommentRead): LiveFeedItem => ({
|
|||||||
|
|
||||||
const toLiveFeedFromBoardChat = (memory: BoardChatMessage): LiveFeedItem => {
|
const toLiveFeedFromBoardChat = (memory: BoardChatMessage): LiveFeedItem => {
|
||||||
const content = (memory.content ?? "").trim();
|
const content = (memory.content ?? "").trim();
|
||||||
const actorName = (memory.source ?? "User").trim() || "User";
|
const actorName = resolveHumanActorName(memory.source, DEFAULT_HUMAN_LABEL);
|
||||||
const isCommand = content.startsWith("/");
|
const isCommand = content.startsWith("/");
|
||||||
return {
|
return {
|
||||||
id: `chat:${memory.id}`,
|
id: `chat:${memory.id}`,
|
||||||
@@ -591,15 +596,16 @@ TaskCommentCard.displayName = "TaskCommentCard";
|
|||||||
|
|
||||||
const ChatMessageCard = memo(function ChatMessageCard({
|
const ChatMessageCard = memo(function ChatMessageCard({
|
||||||
message,
|
message,
|
||||||
|
fallbackSource,
|
||||||
}: {
|
}: {
|
||||||
message: BoardChatMessage;
|
message: BoardChatMessage;
|
||||||
|
fallbackSource: string;
|
||||||
}) {
|
}) {
|
||||||
|
const sourceLabel = resolveHumanActorName(message.source, fallbackSource);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-4">
|
<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">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<p className="text-sm font-semibold text-slate-900">
|
<p className="text-sm font-semibold text-slate-900">{sourceLabel}</p>
|
||||||
{message.source ?? "User"}
|
|
||||||
</p>
|
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-400">
|
||||||
{formatShortTimestamp(message.created_at)}
|
{formatShortTimestamp(message.created_at)}
|
||||||
</span>
|
</span>
|
||||||
@@ -776,6 +782,11 @@ export default function BoardDetailPage() {
|
|||||||
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
|
||||||
return member ? ["owner", "admin"].includes(member.role) : false;
|
return member ? ["owner", "admin"].includes(member.role) : false;
|
||||||
}, [membershipQuery.data]);
|
}, [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 canWrite = boardAccess.canWrite;
|
||||||
|
|
||||||
const [board, setBoard] = useState<Board | null>(null);
|
const [board, setBoard] = useState<Board | null>(null);
|
||||||
@@ -2002,6 +2013,7 @@ export default function BoardDetailPage() {
|
|||||||
{
|
{
|
||||||
content: trimmed,
|
content: trimmed,
|
||||||
tags: ["chat"],
|
tags: ["chat"],
|
||||||
|
source: currentUserDisplayName,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (result.status !== 200) {
|
if (result.status !== 200) {
|
||||||
@@ -2028,7 +2040,7 @@ export default function BoardDetailPage() {
|
|||||||
return { ok: false, error: message };
|
return { ok: false, error: message };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[boardId, isSignedIn, pushLiveFeed],
|
[boardId, currentUserDisplayName, isSignedIn, pushLiveFeed],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSendChat = useCallback(
|
const handleSendChat = useCallback(
|
||||||
@@ -3869,7 +3881,7 @@ export default function BoardDetailPage() {
|
|||||||
authorLabel={
|
authorLabel={
|
||||||
comment.agent_id
|
comment.agent_id
|
||||||
? (assigneeById.get(comment.agent_id) ?? "Agent")
|
? (assigneeById.get(comment.agent_id) ?? "Agent")
|
||||||
: "Admin"
|
: currentUserDisplayName
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -3918,7 +3930,11 @@ export default function BoardDetailPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
chatMessages.map((message) => (
|
chatMessages.map((message) => (
|
||||||
<ChatMessageCard key={message.id} message={message} />
|
<ChatMessageCard
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
fallbackSource={currentUserDisplayName}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
<div ref={chatEndRef} />
|
<div ref={chatEndRef} />
|
||||||
@@ -3983,8 +3999,11 @@ export default function BoardDetailPage() {
|
|||||||
null)
|
null)
|
||||||
: null;
|
: null;
|
||||||
const authorName =
|
const authorName =
|
||||||
item.actor_name?.trim() ||
|
authorAgent?.name ??
|
||||||
(authorAgent ? authorAgent.name : "Admin");
|
resolveHumanActorName(
|
||||||
|
item.actor_name,
|
||||||
|
currentUserDisplayName,
|
||||||
|
);
|
||||||
const authorRole = authorAgent
|
const authorRole = authorAgent
|
||||||
? agentRoleLabel(authorAgent)
|
? agentRoleLabel(authorAgent)
|
||||||
: null;
|
: 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