Compare commits

...

1 Commits

Author SHA1 Message Date
Lambda
a6d3079183 refactor(views): migrate MemberDetailPage to shared BreadcrumbHeader
Aligns the last detail page with the 7 already migrated in PR #3510.
The hand-rolled MemberBreadcrumb (workspace-name root, custom gap-1.5,
missing text-foreground on the leaf) is replaced by the shared
BreadcrumbHeader; the workspace avatar/name no longer renders, and
MemberDetailSkeleton is reshaped to match (segment + chevron + leaf,
no workspace-name placeholder).

- Add wsPaths.members() so the segment href resolves; update
  paths.workspace() shape and the relevant unit tests.
- Drop the now-unused `detail.workspace_fallback` i18n key from all
  three locale files (en/zh-Hans/ko); `members_breadcrumb` and
  `breadcrumb_fallback` remain.

Refs MUL-2851 (parent: MUL-2850).

Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 11:17:13 +08:00
7 changed files with 29 additions and 37 deletions

View File

@@ -22,6 +22,7 @@ describe("paths.workspace() shape", () => {
"projects",
"autopilots",
"agents",
"members",
"squads",
"inbox",
"myIssues",
@@ -43,6 +44,7 @@ describe("paths.workspace() shape", () => {
["projects", "projects"],
["autopilots", "autopilots"],
["agents", "agents"],
["members", "members"],
["squads", "squads"],
["inbox", "inbox"],
["myIssues", "my-issues"],

View File

@@ -13,6 +13,7 @@ describe("paths.workspace(slug)", () => {
expect(ws.autopilots()).toBe("/acme/autopilots");
expect(ws.autopilotDetail("a1")).toBe("/acme/autopilots/a1");
expect(ws.agents()).toBe("/acme/agents");
expect(ws.members()).toBe("/acme/members");
expect(ws.memberDetail("u1")).toBe("/acme/members/u1");
expect(ws.inbox()).toBe("/acme/inbox");
expect(ws.myIssues()).toBe("/acme/my-issues");

View File

@@ -27,6 +27,7 @@ function workspaceScoped(slug: string) {
autopilotDetail: (id: string) => `${ws}/autopilots/${encode(id)}`,
agents: () => `${ws}/agents`,
agentDetail: (id: string) => `${ws}/agents/${encode(id)}`,
members: () => `${ws}/members`,
memberDetail: (id: string) => `${ws}/members/${encode(id)}`,
squads: () => `${ws}/squads`,
squadDetail: (id: string) => `${ws}/squads/${encode(id)}`,

View File

@@ -12,7 +12,6 @@
"more_agents_other": "and {{count}} other agents"
},
"detail": {
"workspace_fallback": "Workspace",
"members_breadcrumb": "Members",
"breadcrumb_fallback": "Member",
"not_found_title": "Member not found",

View File

@@ -12,7 +12,6 @@
"more_agents_other": "외 에이전트 {{count}}개"
},
"detail": {
"workspace_fallback": "워크스페이스",
"members_breadcrumb": "멤버",
"breadcrumb_fallback": "멤버",
"not_found_title": "멤버를 찾을 수 없습니다",

View File

@@ -11,7 +11,6 @@
"more_agents_other": "另有 {{count}} 个智能体"
},
"detail": {
"workspace_fallback": "工作区",
"members_breadcrumb": "成员",
"breadcrumb_fallback": "成员",
"not_found_title": "未找到该成员",

View File

@@ -1,23 +1,22 @@
"use client";
import { ChevronRight, UserRound } from "lucide-react";
import { UserRound } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import type { MemberRole } from "@multica/core/types";
import { useWorkspaceId } from "@multica/core/hooks";
import { useCurrentWorkspace } from "@multica/core/paths";
import { useWorkspacePaths } from "@multica/core/paths";
import { memberListOptions } from "@multica/core/workspace/queries";
import { resolvePublicFileUrl } from "@multica/core/workspace/avatar-url";
import { ActorAvatar as ActorAvatarBase } from "@multica/ui/components/common/actor-avatar";
import { Skeleton } from "@multica/ui/components/ui/skeleton";
import { PageHeader } from "../layout/page-header";
import { WorkspaceAvatar } from "../workspace/workspace-avatar";
import { BreadcrumbHeader } from "../layout/breadcrumb-header";
import { ActorIssuesPanel } from "../common/actor-issues-panel";
import { useT } from "../i18n";
export function MemberDetailPage({ userId }: { userId: string }) {
const { t } = useT("members");
const paths = useWorkspacePaths();
const wsId = useWorkspaceId();
const workspace = useCurrentWorkspace();
const { data: members = [], isLoading } = useQuery(memberListOptions(wsId));
const member = members.find((m) => m.user_id === userId) ?? null;
@@ -28,7 +27,14 @@ export function MemberDetailPage({ userId }: { userId: string }) {
if (!member) {
return (
<div className="flex flex-1 min-h-0 flex-col">
<MemberBreadcrumb workspaceName={workspace?.name} title={t(($) => $.detail.breadcrumb_fallback)} />
<BreadcrumbHeader
segments={[{ href: paths.members(), label: t(($) => $.detail.members_breadcrumb) }]}
leaf={
<span className="truncate font-medium text-foreground">
{t(($) => $.detail.breadcrumb_fallback)}
</span>
}
/>
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 py-16 text-center">
<UserRound className="h-8 w-8 text-muted-foreground" />
<div>
@@ -51,7 +57,14 @@ export function MemberDetailPage({ userId }: { userId: string }) {
return (
<div className="flex flex-1 min-h-0 flex-col">
<MemberBreadcrumb workspaceName={workspace?.name} title={member.name} />
<BreadcrumbHeader
segments={[{ href: paths.members(), label: t(($) => $.detail.members_breadcrumb) }]}
leaf={
<span className="truncate font-medium text-foreground">
{member.name}
</span>
}
/>
<div className="flex shrink-0 items-center gap-3 border-b px-6 py-4">
<ActorAvatarBase
@@ -77,30 +90,6 @@ export function MemberDetailPage({ userId }: { userId: string }) {
);
}
function MemberBreadcrumb({
workspaceName,
title,
}: {
workspaceName: string | undefined;
title: string;
}) {
const { t } = useT("members");
return (
<PageHeader className="gap-1.5">
<WorkspaceAvatar name={workspaceName ?? "W"} size="sm" />
<span className="text-sm text-muted-foreground">
{workspaceName ?? t(($) => $.detail.workspace_fallback)}
</span>
<ChevronRight className="h-3 w-3 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{t(($) => $.detail.members_breadcrumb)}
</span>
<ChevronRight className="h-3 w-3 text-muted-foreground" />
<span className="truncate text-sm font-medium">{title}</span>
</PageHeader>
);
}
function RoleBadge({ role }: { role: MemberRole }) {
const { t } = useT("members");
return (
@@ -117,9 +106,11 @@ function RoleBadge({ role }: { role: MemberRole }) {
function MemberDetailSkeleton() {
return (
<div className="flex flex-1 min-h-0 flex-col">
<PageHeader className="px-5">
<Skeleton className="h-5 w-52" />
</PageHeader>
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-4">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-3" />
<Skeleton className="h-4 w-24" />
</div>
<div className="flex shrink-0 items-center gap-3 border-b px-6 py-4">
<Skeleton className="h-11 w-11 rounded-full" />
<div className="flex-1 space-y-2">