diff --git a/apps/aevatar-console-web/src/pages/studio/index.test.tsx b/apps/aevatar-console-web/src/pages/studio/index.test.tsx index a2354710a..3b60b4561 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.test.tsx @@ -3489,6 +3489,17 @@ describe("StudioPage", () => { }); }); + it("returns to canonical Team detail when Studio has Team context", async () => { + renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + + fireEvent.click(await screen.findByRole("button", { name: "返回团队" })); + + expect(window.location.pathname).toBe("/teams/scope-1/t-alpha"); + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get("memberId")).toBe("workspace-demo"); + expect(searchParams.get("tab")).toBe("advanced"); + }); + it("moves focus from a Script draft to the new Workflow member after create", async () => { (studioApi.getAppContext as jest.Mock).mockResolvedValueOnce({ ...defaultStudioAppContext, @@ -5739,7 +5750,7 @@ describe("StudioPage", () => { }); it("opens the Studio invoke surface from the bind surface endpoint action", async () => { - renderStudioPage("/studio?scopeId=scope-1&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); fireEvent.click(await screen.findByRole("button", { name: "Bind" })); fireEvent.click(await screen.findByRole("button", { name: "Continue to Invoke" })); @@ -5749,6 +5760,11 @@ describe("StudioPage", () => { expect(screen.getByText("service:default")).toBeTruthy(); expect(screen.getByText("endpoint:support-chat")).toBeTruthy(); }); + + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get("teamId")).toBe("t-alpha"); + expect(searchParams.get("member")).toBe("member:workspace-demo"); + expect(searchParams.get("step")).toBe("invoke"); }); it("pins Observe to the selected member service and corrects stale run selection", async () => { diff --git a/apps/aevatar-console-web/src/pages/studio/index.tsx b/apps/aevatar-console-web/src/pages/studio/index.tsx index 1ee5cb5f4..7b7d9f574 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.tsx @@ -3943,6 +3943,7 @@ const StudioPage: React.FC = () => { history.replace( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: boundMemberKey, step: 'bind', }), @@ -3958,6 +3959,7 @@ const StudioPage: React.FC = () => { resolvedStudioScopeId, routeState.memberId, routeState.memberKey, + routeState.teamId, selectedScriptId, selectedWorkflowMemberKey, scopeServicesQuery, @@ -4684,6 +4686,7 @@ const StudioPage: React.FC = () => { history.replace( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, focus: `workflow:${fallbackWorkflowId}`, step: 'build', tab: 'studio', @@ -4694,6 +4697,7 @@ const StudioPage: React.FC = () => { history.replace( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, step: 'build', tab: 'studio', }), @@ -5401,6 +5405,7 @@ const StudioPage: React.FC = () => { history.replace( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: trimOptional(routeState.memberKey) || (resolvedMemberId @@ -5422,6 +5427,7 @@ const StudioPage: React.FC = () => { resolvedStudioScopeId, routeState.memberId, routeState.memberKey, + routeState.teamId, studioScopeMembers, ], ); @@ -6744,6 +6750,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: normalizedMemberKey, step: currentLifecycleStep, }), @@ -6754,6 +6761,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: normalizedMemberKey, tab: 'studio', }), @@ -6783,6 +6791,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: normalizedMemberKey, step: currentLifecycleStep, }), @@ -6793,6 +6802,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: normalizedMemberKey, tab: 'scripts', }), @@ -6851,6 +6861,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: workflowMemberKey, tab: 'studio', }), @@ -6876,6 +6887,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: `script:${scriptId}`, tab: 'scripts', }), @@ -6937,6 +6949,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: selectedMemberId ? `member:${selectedMemberId}` : normalizedMemberKey, step: currentLifecycleStep, @@ -6949,6 +6962,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: selectedMemberOwnerKey, tab: 'studio', }), @@ -6961,6 +6975,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: selectedMemberOwnerKey, tab: 'scripts', }), @@ -6972,6 +6987,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: selectedMemberOwnerKey, step: 'bind', }), @@ -7000,6 +7016,7 @@ const StudioPage: React.FC = () => { publishedScopeMembers, publishedScopeServices, resolvedStudioScopeId, + routeState.teamId, studioSurface, visibleWorkflowSummaries, workbenchMemberKey, @@ -7631,7 +7648,18 @@ const StudioPage: React.FC = () => { .map((value) => trimOptional(value)) .filter(Boolean); const studioReturnHref = resolvedStudioScopeId - ? buildTeamDetailHref({ + ? routeState.teamId + ? buildTeamDetailHref({ + scopeId: resolvedStudioScopeId, + teamId: routeState.teamId, + tab: 'advanced', + memberId: + trimOptional(routeState.memberId) || + readMemberIdFromMemberKey(routeState.memberKey) || + undefined, + serviceId: trimOptional(workbenchPublishedService?.serviceId) || undefined, + }) + : buildTeamDetailHref({ scopeId: resolvedStudioScopeId, tab: 'advanced', serviceId: @@ -7915,6 +7943,7 @@ const StudioPage: React.FC = () => { history.push( buildStudioRoute({ scopeId: resolvedStudioScopeId || undefined, + teamId: routeState.teamId || undefined, memberKey: selectedScriptId ? `script:${selectedScriptId}` : undefined, step: 'bind', tab: 'bindings', diff --git a/apps/aevatar-console-web/src/pages/teams/detail.test.tsx b/apps/aevatar-console-web/src/pages/teams/detail.test.tsx index ca17ed14c..46c0ccfd7 100644 --- a/apps/aevatar-console-web/src/pages/teams/detail.test.tsx +++ b/apps/aevatar-console-web/src/pages/teams/detail.test.tsx @@ -1544,6 +1544,9 @@ describe("TeamDetailPage", () => { level: 1, name: "Alpha Support Team", }); + await waitFor(() => { + expect(studioApi.listTeamMembers).toHaveBeenCalledWith("scope-1", "t-alpha"); + }); await waitFor(() => { expect(scopesApi.getWorkflowDetail).toHaveBeenCalledWith("scope-1", "workflow-1"); }); @@ -1558,9 +1561,9 @@ describe("TeamDetailPage", () => { const params = pushedUrl.searchParams; expect(params.get("scopeId")).toBe("scope-1"); expect(params.get("teamId")).toBe("t-alpha"); - expect(params.get("member")).toBe("workflow:workflow-1"); + expect(params.get("member")).toBe("member:member-team-alpha"); expect(params.get("memberId")).toBeNull(); - expect(params.get("focus")).toBeNull(); + expect(params.get("focus")).toBe("workflow:workflow-1"); expect(params.get("tab")).toBe("studio"); pushSpy.mockRestore(); }); @@ -1571,6 +1574,9 @@ describe("TeamDetailPage", () => { await screen.findByRole("button", { name: "服务映射" }); fireEvent.click(screen.getByRole("button", { name: "Assets" })); await screen.findByText("当前 Team 资产"); + await waitFor(() => { + expect(studioApi.listTeamMembers).toHaveBeenCalledWith("scope-1", "t-alpha"); + }); fireEvent.click(screen.getByRole("button", { name: "打开 workflow Support Escalation Triage" })); @@ -1578,10 +1584,9 @@ describe("TeamDetailPage", () => { expect(window.location.pathname).toBe("/studio"); }); expect(window.location.search).toContain("scopeId=scope-1"); - expect(window.location.search).toContain("member=workflow%3Aworkflow-1"); - expect(window.location.search).not.toContain( - "focus=workflow%3Aworkflow-1", - ); + expect(window.location.search).toContain("teamId=t-alpha"); + expect(window.location.search).toContain("member=member%3Amember-team-alpha"); + expect(window.location.search).toContain("focus=workflow%3Aworkflow-1"); cleanup(); window.history.replaceState({}, "", "/teams/scope-1/t-alpha?tab=assets"); @@ -1594,8 +1599,9 @@ describe("TeamDetailPage", () => { expect(window.location.pathname).toBe("/studio"); }); expect(window.location.search).toContain("scopeId=scope-1"); - expect(window.location.search).toContain("member=script%3Ascript-1"); - expect(window.location.search).not.toContain("focus=script%3Ascript-1"); + expect(window.location.search).toContain("teamId=t-alpha"); + expect(window.location.search).toContain("member=member%3Amember-team-alpha"); + expect(window.location.search).toContain("focus=script%3Ascript-1"); expect(window.location.search).toContain("tab=scripts"); }); diff --git a/apps/aevatar-console-web/src/pages/teams/detail.tsx b/apps/aevatar-console-web/src/pages/teams/detail.tsx index b9a10c8d3..087dd51c7 100644 --- a/apps/aevatar-console-web/src/pages/teams/detail.tsx +++ b/apps/aevatar-console-web/src/pages/teams/detail.tsx @@ -45,6 +45,7 @@ import { buildStudioScriptsWorkspaceRoute, buildStudioWorkflowEditorRoute, buildStudioWorkflowWorkspaceRoute, + resolveStudioMemberRouteKey, } from "@/shared/studio/navigation"; import { formatStudioMemberLifecycleStage, @@ -1476,6 +1477,7 @@ const TeamDetailPage: React.FC = () => { lens.currentService?.serviceId || lens.currentRun?.serviceId || undefined; + const firstTeamRosterMemberId = trimText(teamMembersQuery.data?.members?.[0]?.memberId); const currentMemberId = trimText(preferredMemberSummary?.memberId) || trimText(preferredMemberId); @@ -1522,25 +1524,33 @@ const TeamDetailPage: React.FC = () => { }), [currentPlatformService?.appId, currentPlatformService?.namespace, currentPlatformService?.tenantId, runtimeServiceId, scopeId], ); + const selectedStudioBackendMemberId = + firstTeamRosterMemberId || (hasTeamIdentity ? "" : currentMemberId); + const selectedStudioLegacyMemberId = + hasTeamIdentity + ? "" + : trimText(runtimeServiceId) || + trimText(serviceRevisionsQuery.data?.serviceId) || + trimText(preferredServiceId) || + trimText(servicesQuery.data?.[0]?.serviceId) || + trimText(activeWorkflowSummary?.serviceKey).split(":").pop()?.trim() || + ""; const selectedStudioMemberId = - currentMemberId || - trimText(runtimeServiceId) || - trimText(serviceRevisionsQuery.data?.serviceId) || - trimText(preferredServiceId) || - trimText(servicesQuery.data?.[0]?.serviceId) || - trimText(activeWorkflowSummary?.serviceKey).split(":").pop()?.trim() || - ""; + selectedStudioBackendMemberId || selectedStudioLegacyMemberId; + const selectedStudioWorkflowMemberKey = buildStudioWorkflowMemberKey({ + workflowId: activeWorkflowSummary?.workflowId, + workflowName: + trimText(activeWorkflowSummary?.displayName) || + trimText(activeWorkflowSummary?.workflowName), + }); const selectedStudioMemberKey = - trimText(activeWorkflowSummary?.workflowId).length > 0 - ? buildStudioWorkflowMemberKey({ - workflowId: activeWorkflowSummary?.workflowId, - workflowName: - trimText(activeWorkflowSummary?.displayName) || - trimText(activeWorkflowSummary?.workflowName), - }) - : selectedStudioMemberId - ? `member:${selectedStudioMemberId}` - : undefined; + resolveStudioMemberRouteKey({ + memberId: selectedStudioBackendMemberId, + memberKey: selectedStudioWorkflowMemberKey, + }) || + resolveStudioMemberRouteKey({ + memberId: selectedStudioLegacyMemberId, + }); const teamBuilderRoute = trimText(activeWorkflowSummary?.workflowId).length > 0 @@ -3696,26 +3706,34 @@ const TeamDetailPage: React.FC = () => { history.push( buildStudioWorkflowEditorRoute({ scopeId, - memberKey: - trimText(workflowId).length > 0 ? `workflow:${trimText(workflowId)}` : undefined, + teamId: selectedTeamId || undefined, + memberKey: resolveStudioMemberRouteKey({ + memberId: selectedStudioBackendMemberId, + memberKey: selectedStudioMemberKey, + workflowId, + }), workflowId, }), ); }, - [scopeId], + [scopeId, selectedStudioBackendMemberId, selectedStudioMemberKey, selectedTeamId], ); const handleOpenScriptAsset = React.useCallback( (scriptId: string) => { history.push( buildStudioScriptsWorkspaceRoute({ scopeId, - memberKey: - trimText(scriptId).length > 0 ? `script:${trimText(scriptId)}` : undefined, + teamId: selectedTeamId || undefined, + memberKey: resolveStudioMemberRouteKey({ + memberId: selectedStudioBackendMemberId, + memberKey: selectedStudioMemberKey, + scriptId, + }), scriptId, }), ); }, - [scopeId], + [scopeId, selectedStudioBackendMemberId, selectedStudioMemberKey, selectedTeamId], ); const renderOverviewTab = () => { diff --git a/apps/aevatar-console-web/src/pages/teams/home.test.tsx b/apps/aevatar-console-web/src/pages/teams/home.test.tsx index f33b62526..7899e6e04 100644 --- a/apps/aevatar-console-web/src/pages/teams/home.test.tsx +++ b/apps/aevatar-console-web/src/pages/teams/home.test.tsx @@ -182,7 +182,8 @@ describe("TeamsHomePage", () => { const params = new URLSearchParams(window.location.search); expect(params.get("scopeId")).toBe("scope-a"); - expect(params.get("teamId")).toBeNull(); + expect(params.get("teamId")).toBe("t-support"); + expect(params.get("member")).toBe("member:member-alpha"); expect(params.get("tab")).toBe("studio"); }); diff --git a/apps/aevatar-console-web/src/pages/teams/home.tsx b/apps/aevatar-console-web/src/pages/teams/home.tsx index f7d980466..89cbbe3d0 100644 --- a/apps/aevatar-console-web/src/pages/teams/home.tsx +++ b/apps/aevatar-console-web/src/pages/teams/home.tsx @@ -456,6 +456,7 @@ function buildMemberRosterPreview(input: { readonly runtimeAvailableByMemberId?: ReadonlySet; readonly scopeId: string; readonly services: readonly ServiceCatalogSnapshot[]; + readonly teamId?: string | null; }): MemberRosterPreview { const matchedService = resolveMemberPreviewService({ member: input.member, @@ -485,6 +486,7 @@ function buildMemberRosterPreview(input: { const title = pickMeaningfulLabel(input.member.displayName, input.member.memberId) || "未命名成员"; const studioHref = buildStudioWorkflowWorkspaceRoute({ scopeId: input.scopeId, + teamId: input.teamId || undefined, memberId, }); @@ -586,6 +588,7 @@ function buildTeamRosterPreview(input: { runtimeAvailableByMemberId: input.runtimeAvailableByMemberId, scopeId: input.scopeId, services: input.services, + teamId: input.team.teamId, }), ); const sortedMembers = [...input.members].sort(compareMembers); @@ -640,6 +643,8 @@ function buildTeamRosterPreview(input: { }); const studioHref = buildStudioWorkflowWorkspaceRoute({ scopeId: input.scopeId, + teamId: input.team.teamId, + memberId: primaryMemberPreview?.memberId || undefined, }); const runtimeHref = primaryMemberPreview?.serviceId && primaryMemberPreview.serviceId.length > 0 @@ -1476,6 +1481,7 @@ const TeamsHomePage: React.FC = () => { aria-label="切换到卡片视图" icon={} onClick={() => setManualRosterView("cards")} + style={{ height: 44, width: 44 }} type={resolvedRosterView === "cards" ? "primary" : "default"} /> @@ -1484,6 +1490,7 @@ const TeamsHomePage: React.FC = () => { aria-label="切换到列表视图" icon={} onClick={() => setManualRosterView("list")} + style={{ height: 44, width: 44 }} type={resolvedRosterView === "list" ? "primary" : "default"} /> diff --git a/apps/aevatar-console-web/src/shared/studio/navigation.test.ts b/apps/aevatar-console-web/src/shared/studio/navigation.test.ts index 8b78e33a4..19b749ff6 100644 --- a/apps/aevatar-console-web/src/shared/studio/navigation.test.ts +++ b/apps/aevatar-console-web/src/shared/studio/navigation.test.ts @@ -4,6 +4,7 @@ import { buildStudioScriptsWorkspaceRoute, buildStudioWorkflowEditorRoute, buildStudioWorkflowWorkspaceRoute, + resolveStudioMemberRouteKey, } from './navigation'; describe('buildStudioRoute', () => { @@ -225,3 +226,35 @@ describe('buildStudioRoute', () => { ); }); }); + +describe('resolveStudioMemberRouteKey', () => { + it('prefers backend member identity over implementation identities', () => { + expect( + resolveStudioMemberRouteKey({ + memberId: 'member-alpha', + memberKey: 'workflow:workflow-1', + workflowId: 'workflow-2', + scriptId: 'script-1', + }), + ).toBe('member:member-alpha'); + }); + + it('keeps an existing member key before falling back to workflow or script assets', () => { + expect( + resolveStudioMemberRouteKey({ + memberKey: 'workflow:workflow-1', + scriptId: 'script-1', + }), + ).toBe('workflow:workflow-1'); + expect(resolveStudioMemberRouteKey({ workflowId: 'workflow-2' })).toBe( + 'workflow:workflow-2', + ); + expect(resolveStudioMemberRouteKey({ scriptId: 'script-1' })).toBe( + 'script:script-1', + ); + }); + + it('returns undefined when no stable route identity is available', () => { + expect(resolveStudioMemberRouteKey({ memberId: ' ', memberKey: 'unknown' })).toBeUndefined(); + }); +}); diff --git a/apps/aevatar-console-web/src/shared/studio/navigation.ts b/apps/aevatar-console-web/src/shared/studio/navigation.ts index 4a7c53f79..cc969d2ad 100644 --- a/apps/aevatar-console-web/src/shared/studio/navigation.ts +++ b/apps/aevatar-console-web/src/shared/studio/navigation.ts @@ -63,6 +63,35 @@ export function buildStudioWorkflowMemberKey(options?: { return routeValue ? (`workflow:${routeValue}` as const) : undefined; } +export function resolveStudioMemberRouteKey(input?: { + memberId?: string | null; + memberKey?: StudioMemberKey | string | null; + workflowId?: string | null; + scriptId?: string | null; +}): StudioMemberKey | undefined { + const memberId = trimOptional(input?.memberId); + if (memberId) { + return `member:${memberId}`; + } + + const memberKey = normalizeStudioMemberKey(input?.memberKey); + if (memberKey) { + return memberKey; + } + + const workflowId = trimOptional(input?.workflowId); + if (workflowId) { + return `workflow:${workflowId}`; + } + + const scriptId = trimOptional(input?.scriptId); + if (scriptId) { + return `script:${scriptId}`; + } + + return undefined; +} + function normalizeStudioBuildFocus( value: StudioBuildFocus | string | null | undefined, ): StudioBuildFocus | undefined { diff --git a/apps/aevatar-console-web/src/shared/ui/aevatarPageShells.tsx b/apps/aevatar-console-web/src/shared/ui/aevatarPageShells.tsx index 654571679..ddf203aa0 100644 --- a/apps/aevatar-console-web/src/shared/ui/aevatarPageShells.tsx +++ b/apps/aevatar-console-web/src/shared/ui/aevatarPageShells.tsx @@ -133,6 +133,11 @@ const pageContainerChildrenDocumentStyle: React.CSSProperties = { width: '100%', }; +const compactPageContainerChildrenDocumentStyle: React.CSSProperties = { + ...pageContainerChildrenDocumentStyle, + paddingInline: 0, +}; + const panelInnerViewportStyle: React.CSSProperties = { display: 'flex', flex: 1, @@ -238,49 +243,56 @@ export const AevatarPageShell: React.FC = ({ pageHeaderRender, title, titleHelp, -}) => ( - - - ) : ( - title - ) - } - > -
{ + const screens = Grid.useBreakpoint(); + const useCompactDocumentPadding = layoutMode === 'document' && !screens.md; + + return ( + + + ) : ( + title + ) } > - {children} -
-
-
-); +
+ {children} +
+ + + ); +}; // Default console layout: keep one navigator rail and one primary stage. export const AevatarTwoPaneLayout: React.FC = ({