From 0623ad83a83c864cb614868653e826119162fede Mon Sep 17 00:00:00 2001 From: Rob Lester Date: Thu, 4 Dec 2025 16:28:21 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20contributor=20profile=20?= =?UTF-8?q?editor=20navigation=20to=20posts=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://linear.app/ghost/issue/BER-3039 Contributors who closed the profile editor modal were redirected to /analytics which they cannot access, leaving them stuck on a blank settings page. Now redirects contributors to /posts instead. --- .../settings/general/user-detail-modal.tsx | 3 ++- apps/admin-x-settings/src/main-content.tsx | 10 +++------- .../test/acceptance/permissions.test.ts | 14 ++++++++------ ghost/admin/app/routes/settings-x.js | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx index f8e3e9734a8..fda1a0b8314 100644 --- a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx @@ -210,7 +210,8 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { if (canAccessSettings(currentUser)) { updateRoute('staff'); } else { - updateRoute({isExternal: true, route: 'analytics'}); + // Contributors can't access settings, exit to let the shell handle navigation + updateRoute({isExternal: true, route: ''}); } }, [currentUser, updateRoute]); diff --git a/apps/admin-x-settings/src/main-content.tsx b/apps/admin-x-settings/src/main-content.tsx index d8f51651fbc..665a44a60ff 100644 --- a/apps/admin-x-settings/src/main-content.tsx +++ b/apps/admin-x-settings/src/main-content.tsx @@ -22,7 +22,7 @@ const Page: React.FC<{children: ReactNode}> = ({children}) => { const MainContent: React.FC = () => { const {currentUser} = useGlobalData(); - const {route, updateRoute, loadingModal} = useRouting(); + const {loadingModal} = useRouting(); const {isDirty} = useGlobalDirtyState(); const navigateAway = (escLocation: string) => { @@ -50,12 +50,8 @@ const MainContent: React.FC = () => { toast.remove(); }, []); - useEffect(() => { - if (!canAccessSettings(currentUser) && route !== `staff/${currentUser.slug}`) { - updateRoute(`staff/${currentUser.slug}`); - } - }, [currentUser, route, updateRoute]); - + // Contributors/Authors only see their profile modal (rendered via routing) + // Don't render the main settings content for them if (!canAccessSettings(currentUser)) { return null; } diff --git a/apps/admin-x-settings/test/acceptance/permissions.test.ts b/apps/admin-x-settings/test/acceptance/permissions.test.ts index be656a04285..da086436a74 100644 --- a/apps/admin-x-settings/test/acceptance/permissions.test.ts +++ b/apps/admin-x-settings/test/acceptance/permissions.test.ts @@ -16,20 +16,22 @@ test.describe('User permissions', async () => { await expect(page.getByTestId('title-and-description')).toBeHidden(); }); + // Note: Author/Contributor redirect to profile is handled by the Ember router (settings-x.js), + // not by the React app. These tests verify the UI renders correctly when on the profile route. test('Authors can only see their own profile', async ({page}) => { await mockApi({page, requests: { ...globalDataRequests, browseMe: {...globalDataRequests.browseMe, response: meWithRole('Author')} }}); - await page.goto('/'); + // Navigate directly to profile route using hash-based routing + // (Ember router handles redirect in production) + await page.goto('/#/settings/staff/owner'); await expect(page.getByTestId('user-detail-modal')).toBeVisible(); await expect(page.getByTestId('sidebar')).toBeHidden(); await expect(page.getByTestId('users')).toBeHidden(); await expect(page.getByTestId('title-and-description')).toBeHidden(); - - expect(page.url()).toMatch(/\/owner$/); }); test('Contributors can only see their own profile', async ({page}) => { @@ -38,13 +40,13 @@ test.describe('User permissions', async () => { browseMe: {...globalDataRequests.browseMe, response: meWithRole('Contributor')} }}); - await page.goto('/'); + // Navigate directly to profile route using hash-based routing + // (Ember router handles redirect in production) + await page.goto('/#/settings/staff/owner'); await expect(page.getByTestId('user-detail-modal')).toBeVisible(); await expect(page.getByTestId('sidebar')).toBeHidden(); await expect(page.getByTestId('users')).toBeHidden(); await expect(page.getByTestId('title-and-description')).toBeHidden(); - - expect(page.url()).toMatch(/\/owner$/); }); }); diff --git a/ghost/admin/app/routes/settings-x.js b/ghost/admin/app/routes/settings-x.js index 818c9c5108e..a23576f6f18 100644 --- a/ghost/admin/app/routes/settings-x.js +++ b/ghost/admin/app/routes/settings-x.js @@ -7,6 +7,22 @@ export default class SettingsXRoute extends AuthenticatedRoute { @service ui; @service modals; + beforeModel(transition) { + super.beforeModel(...arguments); + + // Contributors and Authors can only access their own profile in settings + if (this.session.user.isAuthorOrContributor) { + // Check if they're trying to access their own profile route + const subPath = transition.to?.params?.sub; + const ownProfilePath = `staff/${this.session.user.slug}`; + + // Only allow access to their own profile, redirect everything else + if (subPath !== ownProfilePath) { + return this.transitionTo('settings-x.settings-x', ownProfilePath); + } + } + } + activate() { super.activate(...arguments);