- {t("actionLevel.draftFailed")}
+ {draftErrorMessage}
)}
diff --git a/src/frontend/platform/src/pages/SystemPage/components/permission/ImpactDialog.tsx b/src/frontend/platform/src/pages/SystemPage/components/permission/ImpactDialog.tsx
index 90a584076d..59cf50dc19 100644
--- a/src/frontend/platform/src/pages/SystemPage/components/permission/ImpactDialog.tsx
+++ b/src/frontend/platform/src/pages/SystemPage/components/permission/ImpactDialog.tsx
@@ -9,14 +9,17 @@ import {
} from "@/components/bs-ui/dialog"
import type {
PermissionCatalogAction,
+ PermissionCatalogActionChange,
PermissionCatalogDraft,
PermissionCatalogModel,
+ PermissionCatalogModelChange,
PublishPermissionCatalogDraftRequest,
} from "@/controllers/API/permission"
import { formatDate } from "@/util/utils"
import { AlertTriangle } from "lucide-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
+import { actionLabel } from "./actionLabels"
import { formatBlockerMessage } from "./blockerMessages"
interface ImpactDialogProps {
@@ -35,26 +38,6 @@ interface ImpactDialogProps {
now?: Date
}
-interface ImpactMetricProps {
- label: string
- value: number
- testId: string
-}
-
-function ImpactMetric({ label, value, testId }: ImpactMetricProps) {
- return (
- {
expect(onReviewImpact).not.toHaveBeenCalled()
})
+ it("shows the localized business reason returned by draft validation", async () => {
+ onCreateDraft.mockRejectedValueOnce(
+ "Turn off same-level grants for Manager before moving permission management to a higher level.",
+ )
+ render(
+
,
+ )
+
+ await selectMenuOption("actionLevel.change.edit", 3)
+ fireEvent.click(
+ screen.getByRole("button", { name: "actionLevel.publishChanges" }),
+ )
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(
+ "Turn off same-level grants for Manager",
+ )
+ expect(onReviewImpact).not.toHaveBeenCalled()
+ })
+
it("shows resource scope on demand and the inactive marker on the card", async () => {
// The card carries only what the author scans for — name and on/off. The
// scope is one hover away rather than a row of chips on every card.
diff --git a/src/frontend/platform/src/test/f048ModelEditor.test.tsx b/src/frontend/platform/src/test/f048ModelEditor.test.tsx
index 065986f9b6..9a9289f611 100644
--- a/src/frontend/platform/src/test/f048ModelEditor.test.tsx
+++ b/src/frontend/platform/src/test/f048ModelEditor.test.tsx
@@ -72,6 +72,38 @@ const draft = {
assignee_count: 12,
expansion_count: 2,
revocation_count: 3,
+ action_changes: [
+ {
+ action_code: "manage_permission",
+ action_name: "Manage permission",
+ before_level: 3 as const,
+ after_level: 4 as const,
+ before_active: true,
+ after_active: true,
+ },
+ ],
+ model_changes: [
+ {
+ model_key: "manager",
+ model_name: "Manager",
+ kind: "STANDARD" as const,
+ before_level: 3 as const,
+ after_level: 3 as const,
+ added_action_codes: [],
+ removed_action_codes: ["manage_permission"],
+ affected_assignee_count: 12,
+ },
+ {
+ model_key: "collaborator",
+ model_name: "Collaborator",
+ kind: "CUSTOM" as const,
+ before_level: 3 as const,
+ after_level: 4 as const,
+ added_action_codes: [],
+ removed_action_codes: [],
+ affected_assignee_count: 0,
+ },
+ ],
blockers: [],
expires_at: "2026-07-29T13:00:00Z",
},
@@ -417,10 +449,12 @@ describe("ImpactDialog", () => {
/>,
)
- expect(screen.getByTestId("impact-resource-count")).toHaveTextContent("8")
- expect(screen.getByTestId("impact-grant-count")).toHaveTextContent("5")
expect(screen.getByTestId("impact-assignee-count")).toHaveTextContent("12")
- expect(screen.getByTestId("impact-revocation-count")).toHaveTextContent("3")
+ expect(screen.getByText("impact.changeTitle")).toBeInTheDocument()
+ expect(screen.getByText("level.manager")).toBeInTheDocument()
+ expect(screen.getByText("Collaborator")).toBeInTheDocument()
+ expect(screen.getByText("impact.customLevelOnly")).toBeInTheDocument()
+ expect(screen.queryByText("impact.resources")).not.toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "impact.publish" }))
await waitFor(() => {
diff --git a/src/frontend/platform/src/test/f048PermissionRoster.test.tsx b/src/frontend/platform/src/test/f048PermissionRoster.test.tsx
index 8265957adc..8da042e2bb 100644
--- a/src/frontend/platform/src/test/f048PermissionRoster.test.tsx
+++ b/src/frontend/platform/src/test/f048PermissionRoster.test.tsx
@@ -127,7 +127,7 @@ describe("F048 PermissionListTab", () => {
{
...departmentAssignee,
scope: "INHERITED",
- inherited_from: "knowledge_space:space-1",
+ inherited_from: "folder:94661",
editable: false,
},
],
@@ -146,10 +146,40 @@ describe("F048 PermissionListTab", () => {
const row = await screen.findByTestId("permission-assignee-102")
expect(row).toHaveTextContent("scope.inherited")
- expect(row).toHaveTextContent("knowledge_space:space-1")
+ expect(row).toHaveTextContent("roster.parentFolder")
+ expect(row).not.toHaveTextContent("folder:94661")
expect(row).toHaveAttribute("data-editable", "false")
})
+ it("shows the resolved inherited resource name when available", async () => {
+ vi.mocked(getResourcePermissionGrantsApi).mockResolvedValue({
+ data: [
+ {
+ ...departmentAssignee,
+ scope: "INHERITED",
+ inherited_from: "folder:94661",
+ inherited_from_name: "Release Notes",
+ editable: false,
+ },
+ ],
+ page_size: 50,
+ has_more: false,
+ next_cursor: null,
+ })
+
+ render(
+
,
+ )
+
+ const row = await screen.findByTestId("permission-assignee-102")
+ expect(row).toHaveTextContent("Release Notes")
+ expect(row).not.toHaveTextContent("folder:94661")
+ })
+
it("requests only the current-user summary without roster permission", async () => {
render(