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
13 changes: 10 additions & 3 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,17 @@ export const FIXED_APPLICATIONS = () => `Classic Applications`;
export const AI_AGENTS_APPLICATIONS = () => `AI Agents`;
export const AI_APPLICATION_CARD_LIST_ZERO_STATE = () =>
`There are no AI Agents in this workspace.`;
export const ANVIL_APPLICATIONS = () => `Anvil apps`;
// User-facing label for the ANVIL layout system is "Responsive" (APP-15954).
// The internal name stays "Anvil" everywhere — the persisted
// appPositioning.type == ANVIL enum, LayoutSystemTypes.ANVIL, the feature flags
// and the module paths are unchanged.
// Sentence case and "applications" (not "Apps") to match every sibling on the
// same screen — APPLICATIONS, NEW_APP, APPLICATION_CARD_LIST_ZERO_STATE.
// "responsive" stays lowercase mid-sentence: it is a descriptor, not a brand.
export const ANVIL_APPLICATIONS = () => `Responsive applications`;
export const ANVIL_APPLICATION_CARD_LIST_ZERO_STATE = () =>
`There are no Anvil apps in this workspace yet.`;
export const NEW_ANVIL_APP = () => `Anvil app`;
`There are no responsive applications in this workspace.`;
export const NEW_ANVIL_APP = () => `Responsive application`;
export const AI_AGENT_AUTH_SUBTITLE = () =>
`Sign up with any Google account.\n Support for email will be available soon.`;

Expand Down
29 changes: 17 additions & 12 deletions app/client/src/ce/pages/Applications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
} from "ee/actions/workspaceActions";
import type { UpdateApplicationPayload } from "ee/api/ApplicationApi";
import {
AI_AGENTS_APPLICATIONS,
AI_APPLICATION_CARD_LIST_ZERO_STATE,
ANVIL_APPLICATIONS,
ANVIL_APPLICATION_CARD_LIST_ZERO_STATE,
APPLICATIONS,
Expand Down Expand Up @@ -1088,23 +1086,30 @@ export function ApplicationsSection(props: any) {
applications={anvilApplications}
canInviteToWorkspace={canInviteToWorkspace}
deleteApplication={deleteApplication}
emptyStateMessage={
isAiAgentFlowEnabled
? createMessage(AI_APPLICATION_CARD_LIST_ZERO_STATE)
: createMessage(ANVIL_APPLICATION_CARD_LIST_ZERO_STATE)
}
emptyStateMessage={createMessage(
ANVIL_APPLICATION_CARD_LIST_ZERO_STATE,
)}
enableImportExport={enableImportExport}
hasCreateNewApplicationPermission={
hasCreateNewApplicationPermission
}
hasManageWorkspacePermissions={hasManageWorkspacePermissions}
isMobile={isMobile}
onClickAddNewButton={onClickAddNewAppButton}
title={createMessage(
isAiAgentFlowEnabled
? AI_AGENTS_APPLICATIONS
: ANVIL_APPLICATIONS,
)}
// This list holds ANVIL-layout applications, so it is titled
// for them regardless of the AI-agent flag — they were never
// AI agents, they were only filed under that heading because
// the flag was on (APP-15954).
//
// NOTE: the render gate on this block is NOT correct and is
// not fixed here. `isAnvilEnabled` resolves via
// getIsAnvilLayoutEnabled, which reads the RETIRED
// release_anvil_enabled flag — never served, so always false in
// production. The gate therefore collapses to
// isAiAgentFlowEnabled alone, and an org licensed for Anvil but
// without the AI-agent flag never sees this section at all.
// Tracked separately; see APP-15950.
title={createMessage(ANVIL_APPLICATIONS)}
titleTag={PreviewTag}
updateApplicationDispatch={updateApplicationDispatch}
workspaceId={activeWorkspace.id}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { ThemeProvider } from "styled-components";
import "@testing-library/jest-dom";

import store from "store";
import { lightTheme } from "selectors/themeSelectors";
import { LayoutSystemTypes } from "layoutSystems/types";
import { PERMISSION_TYPE } from "ee/utils/permissionHelpers";
import type { ApplicationPayload } from "entities/Application";
import type { Workspace } from "ee/constants/workspaceConstants";
import { ApplicationsSection } from "../index";

// Force the AI-agent FLOW flag (license_ai_agent_enabled) ON. This is the exact
// state that produced APP-15954: with the flag on, the ANVIL application list
// was titled "AI Agents". The INSTANCE flag is left real — it gates the sibling
// classic-application list, which this file does not exercise.
jest.mock("ee/selectors/aiAgentSelectors", () => ({
__esModule: true,
...jest.requireActual("ee/selectors/aiAgentSelectors"),
getIsAiAgentFlowEnabled: jest.fn(() => true),
}));

// Keep the retired Anvil layout flag OFF so the AI-agent flag is the *only*
// thing rendering the ANVIL list. That is the reported configuration, and it
// means the assertions below cannot be satisfied by an unrelated code path.
jest.mock("layoutSystems/anvil/integrations/selectors", () => ({
__esModule: true,
...jest.requireActual("layoutSystems/anvil/integrations/selectors"),
getIsAnvilLayoutEnabled: jest.fn(() => false),
}));

// Everything below is page chrome that has nothing to do with the title
// decision under test. ApplicationCardList and CardList are deliberately NOT
// mocked: the heading has to be produced by the real render chain, otherwise
// this asserts the stub instead of the product.
jest.mock("ee/pages/Applications/WorkspaceAction", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("ee/pages/Applications/WorkspaceMenu", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("ee/pages/Applications/PackageCardList", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("ee/pages/Applications/WorkflowCardList", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("pages/common/ImportModal", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("pages/common/SharedUserList", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("pages/Editor/gitSync/ReconnectDatasourceModal", () => ({
__esModule: true,
default: () => null,
}));
jest.mock(
"../CreateNewAppFromTemplateModal/CreateNewAppFromTemplatesWrapper",
() => ({
__esModule: true,
default: () => null,
}),
);
// ce/pages/Applications/ApplicationCardList imports NoAppsFound back out of
// ee/pages/Applications, which re-exports this very module — a require cycle
// that jest cannot resolve. Stubbing the barrel breaks it, exactly as
// ee/pages/Applications/PackageCardList.test.tsx does.
jest.mock("ee/pages/Applications", () => ({
__esModule: true,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
NoAppsFound: ({ children }: any) => <div>{children}</div>,
}));
jest.mock("pages/Applications/ApplicationCard", () => ({
__esModule: true,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ({ application }: any) => <div>{application.name}</div>,
}));

const WORKSPACE_ID = "test-workspace";

const workspace = {
id: WORKSPACE_ID,
name: "Test Workspace",
userPermissions: [
PERMISSION_TYPE.MANAGE_WORKSPACE,
PERMISSION_TYPE.CREATE_APPLICATION,
],
} as unknown as Workspace;

const anvilApplication = {
id: "anvil-app-1",
name: "Anvil App 1",
workspaceId: WORKSPACE_ID,
applicationDetail: {
appPositioning: { type: LayoutSystemTypes.ANVIL },
},
} as unknown as ApplicationPayload;

const classicApplication = {
id: "classic-app-1",
name: "Classic App 1",
workspaceId: WORKSPACE_ID,
applicationDetail: {
appPositioning: { type: LayoutSystemTypes.FIXED },
},
} as unknown as ApplicationPayload;

const NO_PACKAGES: never[] = [];
const NO_WORKFLOWS: never[] = [];
const WORKSPACES = [workspace];

const renderApplicationsSection = (applications: ApplicationPayload[]) =>
render(
<ThemeProvider theme={lightTheme}>
<Provider store={store}>
<ApplicationsSection
activeWorkspaceId={WORKSPACE_ID}
applications={applications}
packages={NO_PACKAGES}
workflows={NO_WORKFLOWS}
workspaces={WORKSPACES}
/>
</Provider>
</ThemeProvider>,
);

describe("ApplicationsSection - ANVIL application list heading (APP-15954)", () => {
it("titles the ANVIL application list 'Responsive applications', not 'AI Agents', when the AI-agent flow flag is on", () => {
renderApplicationsSection([anvilApplication, classicApplication]);

// The ANVIL application is listed...
expect(screen.getByText("Anvil App 1")).toBeInTheDocument();
// ...under the responsive heading...
expect(screen.getByText("Responsive applications")).toBeInTheDocument();
// ...and never under the AI Agents heading, which is what users reported.
expect(screen.queryByText("AI Agents")).not.toBeInTheDocument();
});

it("uses the responsive empty-state copy when the workspace has no ANVIL applications", () => {
renderApplicationsSection([classicApplication]);

expect(
screen.getByText(
"There are no responsive applications in this workspace.",
),
).toBeInTheDocument();
expect(
screen.queryByText("There are no AI Agents in this workspace."),
).not.toBeInTheDocument();
});
});
Loading