-
-
Notifications
You must be signed in to change notification settings - Fork 11.4k
Remove members-forward route #27040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove members-forward route #27040
Changes from 17 commits
006d371
bc05ca9
171015d
4ad2087
fb4107c
ea2bff4
60dca51
c62d580
473f258
892eac6
a02a97b
866378d
44321fa
5723454
b075b69
996f9a6
1084372
c708e85
9f9400b
90aea91
607038c
501f508
00d337a
db66fc1
1557afb
9ff9e03
041886b
84466ce
54338d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,11 +11,13 @@ import { useNavigationExpanded } from "./hooks/use-navigation-preferences"; | |
| import { NavCustomViews } from "./nav-custom-views"; | ||
| import { NavMemberViews } from "./nav-member-views"; | ||
| import { useMemberSidebarViews } from "./member-sidebar-views"; | ||
| import { getMembersNavActiveRoutes, isMembersNavActive } from "./nav-content.helpers"; | ||
| import { isMembersNavActive } from "./nav-content.helpers"; | ||
| import { useCustomSidebarViews } from "./use-custom-sidebar-views"; | ||
| import { useEmberRouting } from "@/ember-bridge"; | ||
| import { useFeatureFlag } from "@/hooks/use-feature-flag"; | ||
|
|
||
| const LEGACY_MEMBERS_ACTIVE_ROUTES = ['members', 'member', 'member.new']; | ||
|
|
||
| function PostsNavItemContent({isActive, to}: {isActive: boolean; to: string}) { | ||
| return ( | ||
| <> | ||
|
|
@@ -86,20 +88,20 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) { | |
| const isPublishedPostsRouteActive = routing.isRouteActive('posts', {type: 'published'}); | ||
| const hasActivePostChild = isDraftPostsRouteActive || isScheduledPostsRouteActive || isPublishedPostsRouteActive || postCustomViews.some(view => view.isActive); | ||
| const postsExpanded = savedPostsExpanded; | ||
| const isOnMembersForward = location.pathname === '/members-forward'; | ||
| const hasActiveMemberView = isOnMembersForward && memberViews.some(view => view.isActive); | ||
| const isOnMembersRoute = location.pathname === '/members' || location.pathname === '/members/import'; | ||
|
||
| const hasActiveMemberView = memberViews.some(view => view.isActive); | ||
| const membersExpanded = savedMembersExpanded; | ||
| const membersNavActive = isMembersNavActive({ | ||
| membersForwardEnabled, | ||
| isOnMembersForward, | ||
| isOnMembersRoute, | ||
| hasActiveMemberView, | ||
| isMembersExpanded: membersExpanded, | ||
| isLegacyMembersRouteActive: routing.isRouteActive(getMembersNavActiveRoutes()) | ||
| isLegacyMembersRouteActive: routing.isRouteActive(LEGACY_MEMBERS_ACTIVE_ROUTES) | ||
| }); | ||
| const postsRoute = routing.getRouteUrl('posts'); | ||
| const isPostsRouteActive = routing.isRouteActive('posts'); | ||
| const postsNavActive = isPostsRouteActive || (!postsExpanded && hasActivePostChild); | ||
| const membersRoute = membersForwardEnabled ? 'members-forward' : routing.getRouteUrl('members'); | ||
| const membersRoute = routing.getRouteUrl('members'); | ||
jonatansberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <SidebarGroup {...props}> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import {render, screen} from '@testing-library/react'; | ||
| import React from 'react'; | ||
| import {beforeEach, describe, expect, it, vi} from 'vitest'; | ||
| import {MembersRouteGate} from './members-route-gate'; | ||
|
|
||
| const {mockUseLocation, mockUseFeatureFlag} = vi.hoisted(() => ({ | ||
| mockUseLocation: vi.fn(), | ||
| mockUseFeatureFlag: vi.fn() | ||
| })); | ||
|
|
||
| vi.mock('@tryghost/admin-x-framework', () => ({ | ||
| Outlet: () => React.createElement('div', {'data-testid': 'outlet'}), | ||
| useLocation: mockUseLocation | ||
| })); | ||
|
|
||
| vi.mock('./ember-bridge', () => ({ | ||
| EmberFallback: () => React.createElement('div', {'data-testid': 'ember-fallback'}) | ||
| })); | ||
|
|
||
| vi.mock('./hooks/use-feature-flag', () => ({ | ||
| useFeatureFlag: mockUseFeatureFlag | ||
| })); | ||
|
|
||
| describe('MembersRouteGate', () => { | ||
| beforeEach(() => { | ||
| mockUseFeatureFlag.mockReturnValue(false); | ||
| mockUseLocation.mockReturnValue({pathname: '/members'}); | ||
| }); | ||
|
|
||
| it('delegates /members/ to Ember when the flag is disabled', () => { | ||
| mockUseLocation.mockReturnValue({pathname: '/members/'}); | ||
|
|
||
| render(<MembersRouteGate />); | ||
|
|
||
| expect(screen.getByTestId('ember-fallback')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('delegates /members/import to Ember when the flag is disabled', () => { | ||
| mockUseLocation.mockReturnValue({pathname: '/members/import'}); | ||
|
|
||
| render(<MembersRouteGate />); | ||
|
|
||
| expect(screen.getByTestId('ember-fallback')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders React routes when the flag is enabled', () => { | ||
| mockUseFeatureFlag.mockReturnValue(true); | ||
| mockUseLocation.mockReturnValue({pathname: '/members/import'}); | ||
|
|
||
| render(<MembersRouteGate />); | ||
|
|
||
| expect(screen.getByTestId('outlet')).toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import {Outlet} from "@tryghost/admin-x-framework"; | ||
| import {EmberFallback} from "./ember-bridge"; | ||
| import {useFeatureFlag} from "./hooks/use-feature-flag"; | ||
|
|
||
| export function MembersRouteGate() { | ||
| const membersForwardEnabled = useFeatureFlag("membersForward"); | ||
|
|
||
| if (!membersForwardEnabled) { | ||
| return <EmberFallback />; | ||
jonatansberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return <Outlet />; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
jonatansberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import {render, screen} from '@testing-library/react'; | ||
| import React from 'react'; | ||
| import {beforeEach, describe, expect, it, vi} from 'vitest'; | ||
| import {MembersRoute} from './members-route'; | ||
|
|
||
| const {mockCanManageMembers, mockUseCurrentUser} = vi.hoisted(() => ({ | ||
| mockCanManageMembers: vi.fn(), | ||
| mockUseCurrentUser: vi.fn() | ||
| })); | ||
|
|
||
| vi.mock('@tryghost/admin-x-framework', () => ({ | ||
| Navigate: ({replace, to}: {replace?: boolean; to: string}) => React.createElement('div', { | ||
| 'data-replace': String(Boolean(replace)), | ||
| 'data-testid': 'navigate', | ||
| 'data-to': to | ||
| }) | ||
| })); | ||
|
|
||
| vi.mock('@tryghost/admin-x-framework/api/current-user', () => ({ | ||
| useCurrentUser: mockUseCurrentUser | ||
| })); | ||
|
|
||
| vi.mock('@tryghost/admin-x-framework/api/users', () => ({ | ||
| canManageMembers: mockCanManageMembers | ||
| })); | ||
|
|
||
| vi.mock('./members-route-gate', () => ({ | ||
| MembersRouteGate: () => React.createElement('div', {'data-testid': 'members-route-gate'}) | ||
| })); | ||
|
|
||
| describe('MembersRoute', () => { | ||
| beforeEach(() => { | ||
| mockCanManageMembers.mockReturnValue(true); | ||
| mockUseCurrentUser.mockReturnValue({ | ||
| data: { | ||
| id: '1', | ||
| roles: [{name: 'Administrator'}] | ||
| }, | ||
| isError: false, | ||
| isLoading: false | ||
| }); | ||
| }); | ||
|
|
||
| it('renders the members route gate for authorized users', () => { | ||
| render(<MembersRoute />); | ||
|
|
||
| expect(screen.getByTestId('members-route-gate')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('redirects users without member permissions to home', () => { | ||
| mockCanManageMembers.mockReturnValue(false); | ||
|
|
||
| render(<MembersRoute />); | ||
|
|
||
| expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/'); | ||
| expect(screen.getByTestId('navigate')).toHaveAttribute('data-replace', 'true'); | ||
| }); | ||
|
|
||
| it('renders nothing while the current user is still loading', () => { | ||
| mockUseCurrentUser.mockReturnValue({ | ||
| data: undefined, | ||
| isError: false, | ||
| isLoading: true | ||
| }); | ||
|
|
||
| const {container} = render(<MembersRoute />); | ||
|
|
||
| expect(container).toBeEmptyDOMElement(); | ||
| }); | ||
|
|
||
| it('redirects to home when the current user is unavailable after loading', () => { | ||
| mockUseCurrentUser.mockReturnValue({ | ||
| data: undefined, | ||
| isError: false, | ||
| isLoading: false | ||
| }); | ||
|
|
||
| render(<MembersRoute />); | ||
|
|
||
| expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import {Navigate} from "@tryghost/admin-x-framework"; | ||
| import {useCurrentUser} from "@tryghost/admin-x-framework/api/current-user"; | ||
| import {canManageMembers} from "@tryghost/admin-x-framework/api/users"; | ||
| import {MembersRouteGate} from "./members-route-gate"; | ||
|
|
||
| export function MembersRoute() { | ||
| const {data: currentUser, isError, isLoading} = useCurrentUser(); | ||
|
|
||
| if (!currentUser) { | ||
| if (isError || !isLoading) { | ||
| return <Navigate replace to="/" />; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| if (!canManageMembers(currentUser)) { | ||
| return <Navigate replace to="/" />; | ||
| } | ||
|
|
||
| return <MembersRouteGate />; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The members route gate now explicitly treats
/members/as the members list, but this check only matches'/members', so a trailing-slash URL leaves all member saved views inactive even when the filter matches. This creates inconsistent sidebar state for bookmarked/manual/members/URLs; normalizing trailing slashes here (as done in the gate) avoids that mismatch.Useful? React with 👍 / 👎.