diff --git a/docs/content/docs/guides/team-roles.mdx b/docs/content/docs/guides/team-roles.mdx index ed1cabc89..fee8e392d 100644 --- a/docs/content/docs/guides/team-roles.mdx +++ b/docs/content/docs/guides/team-roles.mdx @@ -69,7 +69,7 @@ What a restricted member sees: - **Inbox**: the conversations of their mailboxes, with counts and filters to match. - **Live updates** for those resources only. They do not appear in the team's presence and do not see who else is online. -Everything that cannot yet be narrowed to a scope is unavailable to them: contacts, CRM, templates, forms, automations, deliverability, placement tests, settings other than their own profile and security, the AI assistant, and the Slack app. They are not sent workspace notifications. An API key a restricted member already holds, and an app they authorized, are held to the same scope and the same read-only routes; they cannot create new keys. +Everything that cannot yet be narrowed to a scope is unavailable to them, and shows as locked in their sidebar with the reason: contacts, CRM, templates, forms, automations, deliverability, placement tests, settings other than their own profile and security, the AI assistant, and the Slack app. They are not sent workspace notifications. An API key a restricted member already holds, and an app they authorized, are held to the same scope and the same read-only routes; they cannot create new keys. Changing a member's access needs **Manage team** and a recent sign-in, takes effect on their next request, and applies to their open dashboard and live updates without a reload. You must hold every permission the member's roles grant, the owner always reaches the entire workspace, and you cannot change your own access. Grants on a deleted folder, campaign or mailbox disappear with it. diff --git a/qa/flows/member-access.flow.ts b/qa/flows/member-access.flow.ts index ec1bcb673..43836c8d9 100644 --- a/qa/flows/member-access.flow.ts +++ b/qa/flows/member-access.flow.ts @@ -56,7 +56,12 @@ test.describe("member access scope", () => { await expect(page.getByRole("link", { name: /Q1 Outreach/ })).toHaveCount(0); await expect(page.getByRole("link", { name: "Contacts" })).toHaveCount(0); await proof.chapter("Restricted teammate", "Only the granted campaigns and mailboxes, and only the pages that can be narrowed to them"); - await proof.shot("restricted-campaigns", { caption: "The sidebar keeps Inbox, Campaigns and Analytics; the list holds only the granted campaigns" }); + await proof.shot("restricted-campaigns", { caption: "Inbox, Campaigns and Analytics stay open, every other page is locked; the list holds only the granted campaigns" }); + + await page.getByRole("complementary").getByRole("button", { name: "Contacts", exact: true }).click(); + await expect(page.getByText("Your access to this workspace is limited to selected campaigns and mailboxes")).toBeVisible(); + await proof.shot("restricted-locked", { caption: "A locked page says the access scope is why, and who can change it" }); + await page.getByRole("button", { name: "Got it" }).click(); await page.getByRole("link", { name: /RevOps outreach - July/ }).first().click(); const main = page.getByRole("main"); diff --git a/web/src/components/layout/AccessLockedDialog.tsx b/web/src/components/layout/AccessLockedDialog.tsx index 5fea8a105..aac4de7a7 100644 --- a/web/src/components/layout/AccessLockedDialog.tsx +++ b/web/src/components/layout/AccessLockedDialog.tsx @@ -12,12 +12,15 @@ export default function AccessLockedDialog({ onClose, feature, permissionLabel = "the required", + reason = "permission", }: { open: boolean; onClose: () => void; feature: string; /** The friendly permission name they're missing. */ permissionLabel?: string; + /** "scope" when the member is restricted to selected resources rather than missing a permission. */ + reason?: "permission" | "scope"; }) { // Portal to : this dialog renders inside the sidebar