Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/aevatar-console-web/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default [
path: "/teams/new",
name: "Create Team",
component: "./teams/new",
hideInMenu: true,
menuGroupKey: "teams",
},
{
Expand Down
57 changes: 57 additions & 0 deletions apps/aevatar-console-web/src/app.layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import defaultSettings from "../config/defaultSettings";
import { layout } from "./app";

describe("layout menu collapse behavior", () => {
beforeEach(() => {
window.history.replaceState({}, "", "/teams");
});

it("keeps grouped navigation titles hidden in collapsed mode", () => {
const runtimeLayout = layout({
initialState: {
Expand All @@ -20,6 +24,59 @@ describe("layout menu collapse behavior", () => {
});
});

it("collapses the global menu for Studio create-member intent", () => {
window.history.replaceState(
{},
"",
"/studio?tab=studio&intent=create-member",
);

const runtimeLayout = layout({
initialState: {
auth: {} as never,
settings: defaultSettings,
},
});

expect(runtimeLayout.defaultCollapsed).toBe(true);
expect(runtimeLayout.collapsed).toBe(true);
});

it("leaves the global menu uncontrolled for ordinary Studio entry", () => {
window.history.replaceState({}, "", "/studio?tab=studio");

const runtimeLayout = layout({
initialState: {
auth: {} as never,
settings: defaultSettings,
},
});

expect(runtimeLayout.defaultCollapsed).toBe(false);
expect(runtimeLayout.collapsed).toBeUndefined();
});

it("updates the controlled global menu collapse state after SPA route changes", () => {
window.history.replaceState({}, "", "/teams?scopeId=scope-a");
const teamsLayout = layout({
initialState: {
auth: {} as never,
settings: defaultSettings,
},
});

window.history.pushState({}, "", "/studio?tab=studio&intent=create-member");
const studioLayout = layout({
initialState: {
auth: {} as never,
settings: defaultSettings,
},
});

expect(teamsLayout.collapsed).toBeUndefined();
expect(studioLayout.collapsed).toBe(true);
});

it("styles collapsed menu items without icons as visible tokens", () => {
const globalStyles = fs.readFileSync(
path.resolve(__dirname, "./global.less"),
Expand Down
18 changes: 18 additions & 0 deletions apps/aevatar-console-web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ function isStudioHostRoute(pathname: string): boolean {
return STUDIO_HOST_ROUTES.has(pathname);
}

function shouldDefaultCollapseLayout(pathname: string, search: string): boolean {
if (!isStudioHostRoute(pathname)) {
return false;
}

return new URLSearchParams(search).get("intent") === "create-member";
}

function shouldCollapseLayout(pathname: string, search: string): boolean {
return shouldDefaultCollapseLayout(pathname, search);
}

function buildLoginRoute(returnTo: string): string {
const params = new URLSearchParams({
redirect: sanitizeReturnTo(returnTo),
Expand Down Expand Up @@ -642,6 +654,10 @@ const AuthSessionBootstrap: React.FC<AuthSessionBootstrapProps> = ({
export const layout = ({
initialState,
}: LayoutRuntimeProps): Record<string, unknown> => {
const pathname = window.location.pathname;
const search = window.location.search;
const collapseForRoute = shouldCollapseLayout(pathname, search);

return {
onPageChange: () => {
const pathname = window.location.pathname;
Expand Down Expand Up @@ -814,6 +830,8 @@ export const layout = ({
overflow: "hidden",
padding: 0,
},
defaultCollapsed: shouldDefaultCollapseLayout(pathname, search),
...(collapseForRoute ? { collapsed: true } : {}),
logo: <BrandLogo />,
};
};
Expand Down
19 changes: 19 additions & 0 deletions apps/aevatar-console-web/src/pages/studio/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,25 @@ describe("StudioPage", () => {
});
});

it("opens the create-member modal once from the typed Studio intent", async () => {
renderStudioPage("/studio?tab=studio&intent=create-member");

const createDialog = await screen.findByRole("dialog", { name: "Create member" });
expect(within(createDialog).getByLabelText("Member name")).toHaveValue("draft");
expect(studioApi.saveWorkflow).not.toHaveBeenCalled();

fireEvent.click(within(createDialog).getByRole("button", { name: "Cancel" }));

await waitFor(() => {
expect(screen.queryByRole("dialog", { name: "Create member" })).toBeNull();
});

await waitFor(() => {
expect(screen.queryByRole("dialog", { name: "Create member" })).toBeNull();
});
expect(studioApi.saveWorkflow).not.toHaveBeenCalled();
});

it("shows script and gagent as member kinds before their create APIs land", async () => {
renderStudioPage("/studio?focus=workflow%3Aworkflow-1&tab=studio");

Expand Down
46 changes: 46 additions & 0 deletions apps/aevatar-console-web/src/pages/studio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
buildStudioRoute,
resolveStudioWorkflowMemberRouteValue,
type StudioBuildFocus,
type StudioIntent,
type StudioStep,
type StudioTab,
} from '@/shared/studio/navigation';
Expand Down Expand Up @@ -135,6 +136,7 @@ type StudioRouteState = {
step: StudioStep;
focusKey: string;
tab: StudioTab;
intent: StudioIntent | '';
prompt: string;
executionId: string;
logsMode: '' | 'popout';
Expand Down Expand Up @@ -892,6 +894,10 @@ function buildWorkflowFileName(workflowName: string): string {
return `${normalizedWorkflowName}.yaml`;
}

function parseStudioIntent(value: string | null | undefined): StudioIntent | '' {
return trimOptional(value) === 'create-member' ? 'create-member' : '';
}

function readWorkflowMemberRouteValueFromMemberKey(memberKey: string): string {
const normalizedMemberKey = trimOptional(memberKey);
if (!normalizedMemberKey.startsWith('workflow:')) {
Expand Down Expand Up @@ -952,6 +958,7 @@ function readStudioRouteState(search?: string): StudioRouteState {
step: 'build',
focusKey: '',
tab: 'workflows',
intent: '',
prompt: '',
executionId: '',
logsMode: '',
Expand All @@ -974,6 +981,7 @@ function readStudioRouteState(search?: string): StudioRouteState {
step: parseStudioStep(params.get('step')),
focusKey: buildFocus.key,
tab: parseStudioTab(params.get('tab')),
intent: parseStudioIntent(params.get('intent')),
prompt: trimOptional(params.get('prompt')),
executionId: trimOptional(params.get('execution')),
logsMode: parseLogsMode(params.get('logs')),
Expand Down Expand Up @@ -1346,6 +1354,12 @@ const StudioPage: React.FC = () => {
const [appliedRouteSnapshot, setAppliedRouteSnapshot] = useState(
locationSnapshot,
);
const [pendingCreateMemberIntentSnapshot, setPendingCreateMemberIntentSnapshot] =
useState(() =>
readStudioRouteState().intent === 'create-member'
? getLocationSnapshot()
: '',
);
const [promptHistory, setPromptHistory] = useState<
PlaygroundPromptHistoryEntry[]
>(() => loadPlaygroundPromptHistory());
Expand All @@ -1365,6 +1379,7 @@ const StudioPage: React.FC = () => {
});
const scriptLeaveGuardRef = useRef<(() => Promise<boolean>) | null>(null);
const handledLocationSnapshotRef = useRef(locationSnapshot);
const handledCreateMemberIntentSnapshotRef = useRef('');
const executionLogsWindowRef = useRef<Window | null>(null);
const [logsDetached, setLogsDetached] = useState(false);
const [authRecoveryPending, setAuthRecoveryPending] = useState(false);
Expand Down Expand Up @@ -1393,6 +1408,9 @@ const StudioPage: React.FC = () => {
setAppliedRouteSnapshot((currentSnapshot) =>
currentSnapshot === locationSnapshot ? currentSnapshot : locationSnapshot,
);
if (routeState.intent === 'create-member') {
setPendingCreateMemberIntentSnapshot(locationSnapshot);
}
setStudioSurface((currentSurface) =>
currentSurface === routeStudioSurface ? currentSurface : routeStudioSurface,
);
Expand Down Expand Up @@ -1440,6 +1458,7 @@ const StudioPage: React.FC = () => {
}, [
locationSnapshot,
routeState.executionId,
routeState.intent,
routeSelectedMember.kind,
routeSelectedMember.value,
routeState.prompt,
Expand Down Expand Up @@ -3031,6 +3050,33 @@ const StudioPage: React.FC = () => {
suggestedCreateWorkflowName,
]);

useEffect(() => {
if (!isStudioLocation || !pendingCreateMemberIntentSnapshot) {
return;
}

if (!studioHostReady || createMemberModalOpen) {
return;
}

if (
handledCreateMemberIntentSnapshotRef.current ===
pendingCreateMemberIntentSnapshot
) {
return;
}

handledCreateMemberIntentSnapshotRef.current = pendingCreateMemberIntentSnapshot;
setPendingCreateMemberIntentSnapshot('');
void openCreateMemberFlow();
}, [
createMemberModalOpen,
isStudioLocation,
openCreateMemberFlow,
pendingCreateMemberIntentSnapshot,
studioHostReady,
]);

const closeCreateMemberFlow = useCallback(() => {
if (inventoryBusyKey === 'create') {
return;
Expand Down
14 changes: 14 additions & 0 deletions apps/aevatar-console-web/src/pages/teams/home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ describe("TeamsHomePage", () => {
expect(params.get("scopeLabel")).toBeNull();
});

it("routes Create Team directly into Studio member creation", async () => {
renderWithQueryClient(React.createElement(TeamsHomePage));

fireEvent.click(await screen.findByRole("button", { name: "组建新团队" }));

expect(window.location.pathname).toBe("/studio");
const params = new URLSearchParams(window.location.search);
expect(params.get("scopeId")).toBe("scope-a");
expect(params.get("tab")).toBe("studio");
expect(params.get("intent")).toBe("create-member");
expect(params.get("teamName")).toBeNull();
expect(params.get("entryName")).toBeNull();
});

it("does not show the roster view toggle when the homepage only has one visible team", async () => {
renderWithQueryClient(React.createElement(TeamsHomePage));

Expand Down
25 changes: 20 additions & 5 deletions apps/aevatar-console-web/src/pages/teams/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { loadRestorableAuthSession } from "@/shared/auth/session";
import { formatCompactDateTime } from "@/shared/datetime/dateTime";
import { history } from "@/shared/navigation/history";
import {
buildTeamCreateHref,
buildTeamDetailHref,
} from "@/shared/navigation/teamRoutes";
import { buildRuntimeRunsHref } from "@/shared/navigation/runtimeRoutes";
Expand All @@ -37,6 +36,7 @@ import {
type StudioScopeBindingStatus,
} from "@/shared/studio/models";
import {
buildStudioRoute,
buildStudioWorkflowMemberKey,
buildStudioWorkflowEditorRoute,
buildStudioWorkflowWorkspaceRoute,
Expand All @@ -62,7 +62,6 @@ import {
type WorkflowOperationalAttention,
} from "./workflowOperationalUnits";

const initialDraft = readScopeQueryDraft();
const scopeServiceAppId = "default";
const scopeServiceNamespace = "default";
const compactTeamRosterThreshold = 6;
Expand Down Expand Up @@ -823,8 +822,12 @@ const ScopeBackedTeamRow: React.FC<{

const TeamsHomePage: React.FC = () => {
const { token } = theme.useToken();
const [draft, setDraft] = React.useState<ScopeQueryDraft>(initialDraft);
const [activeDraft, setActiveDraft] = React.useState<ScopeQueryDraft>(initialDraft);
const [draft, setDraft] = React.useState<ScopeQueryDraft>(() =>
readScopeQueryDraft(),
);
const [activeDraft, setActiveDraft] = React.useState<ScopeQueryDraft>(() =>
readScopeQueryDraft(),
);
const [manualRosterView, setManualRosterView] = React.useState<
"cards" | "list" | null
>(null);
Expand Down Expand Up @@ -1081,7 +1084,19 @@ const TeamsHomePage: React.FC = () => {
<Space wrap>
<Button
icon={<PlusOutlined />}
onClick={() => history.push(buildTeamCreateHref())}
onClick={() =>
history.push(
buildStudioRoute({
scopeId:
scopeId ||
readScopeQueryDraft().scopeId ||
resolvedScope?.scopeId ||
localScopeId,
tab: "studio",
intent: "create-member",
}),
)
}
style={{ borderRadius: 16, height: 40, paddingInline: 18 }}
type="primary"
>
Expand Down
Loading
Loading