Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
006d371
Changed members routes to use feature-gated rendering
jonatansberg Apr 1, 2026
bc05ca9
Fixed member e2e routes after suite reshuffle
jonatansberg Apr 1, 2026
171015d
Reduced duplicate members route tests
jonatansberg Apr 1, 2026
4ad2087
Simplified members nav active route handling
jonatansberg Apr 1, 2026
fb4107c
Restored members React e2e coverage shape
jonatansberg Apr 1, 2026
ea2bff4
Fixed members e2e search selector
jonatansberg Apr 1, 2026
60dca51
Fixed members review issues and e2e scoping
jonatansberg Apr 1, 2026
c62d580
Fixed members route regressions after rebase
jonatansberg Apr 2, 2026
473f258
Fixed members e2e helper scoping after rebase
jonatansberg Apr 2, 2026
892eac6
Fixed members-forward redirect assertion
jonatansberg Apr 2, 2026
a02a97b
Fixed members legacy e2e failures
jonatansberg Apr 2, 2026
866378d
Prevented dual members import modals
jonatansberg Apr 2, 2026
44321fa
Fixed shade imports after rebase
jonatansberg Apr 7, 2026
5723454
Added members route access control
jonatansberg Apr 7, 2026
b075b69
Fixed legacy import mapping assertions
jonatansberg Apr 7, 2026
996f9a6
Fixed legacy import redirect assertion
jonatansberg Apr 7, 2026
1084372
Fixed legacy import hash-route assertion
jonatansberg Apr 7, 2026
c708e85
Fixed members import route handoff
jonatansberg Apr 7, 2026
9f9400b
Fixed members saved views e2e selectors
jonatansberg Apr 7, 2026
90aea91
Simplified members sidebar active state
jonatansberg Apr 7, 2026
607038c
Fixed members import modal ownership gating
jonatansberg Apr 7, 2026
501f508
Removed stale inAdminForward import gating
jonatansberg Apr 7, 2026
00d337a
Removed trivial members route gate test
jonatansberg Apr 7, 2026
db66fc1
Reduced duplicate members test churn
jonatansberg Apr 7, 2026
1557afb
Cleaned members e2e duplication
jonatansberg Apr 7, 2026
9ff9e03
Removed redundant members import modal gate
jonatansberg Apr 7, 2026
041886b
Stopped Ember members import modal rendering on React route
jonatansberg Apr 7, 2026
84466ce
Simplified members nav state and Ember list opt-out
jonatansberg Apr 8, 2026
54338d5
Reduced members list page test churn
jonatansberg Apr 8, 2026
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: 8 additions & 5 deletions apps/admin/src/layout/app-sidebar/member-sidebar-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ function isMemberSidebarView(view: SharedView): view is MemberSidebarView {
}

function getMemberViewUrl(filter: string) {
return `members-forward?${new URLSearchParams({filter}).toString()}`;
return `members?${new URLSearchParams({filter}).toString()}`;
}

function isMemberViewActive(currentSearch: string, filter: string) {
function isMemberViewActive(pathname: string, currentSearch: string, filter: string) {
if (pathname !== '/members') {
return false;
}

return new URLSearchParams(currentSearch).get('filter') === filter;
}

export function useMemberSidebarViews() {
const location = useLocation();
const sharedViews = useSharedViews('members');
const isOnMembersForward = location.pathname === '/members-forward';

return useMemo<NavSavedView[]>(() => {
return sharedViews
Expand All @@ -34,7 +37,7 @@ export function useMemberSidebarViews() {
key: `${view.name}:${view.filter.filter}`,
name: view.name,
to: getMemberViewUrl(view.filter.filter),
isActive: isOnMembersForward && isMemberViewActive(location.search, view.filter.filter)
isActive: isMemberViewActive(location.pathname, location.search, view.filter.filter)
}));
}, [isOnMembersForward, location.search, sharedViews]);
}, [location.pathname, location.search, sharedViews]);
}
55 changes: 0 additions & 55 deletions apps/admin/src/layout/app-sidebar/nav-content.helpers.test.ts

This file was deleted.

32 changes: 0 additions & 32 deletions apps/admin/src/layout/app-sidebar/nav-content.helpers.ts

This file was deleted.

22 changes: 7 additions & 15 deletions apps/admin/src/layout/app-sidebar/nav-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react"

import {SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuBadge} from "@tryghost/shade/components"
import {formatNumber, LucideIcon} from "@tryghost/shade/utils"
import { useLocation } from "@tryghost/admin-x-framework";
import { useCurrentUser } from "@tryghost/admin-x-framework/api/current-user";
import { canManageMembers, canManageTags } from "@tryghost/admin-x-framework/api/users";
import { NavMenuItem } from "./nav-menu-item";
Expand All @@ -11,7 +10,6 @@ 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 { useCustomSidebarViews } from "./use-custom-sidebar-views";
import { useEmberRouting } from "@/ember-bridge";
import { useFeatureFlag } from "@/hooks/use-feature-flag";
Expand Down Expand Up @@ -72,12 +70,12 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
const [savedMembersExpanded, setMembersExpanded] = useNavigationExpanded('members');
const postCustomViews = useCustomSidebarViews('posts');
const memberViews = useMemberSidebarViews();
const hasMemberViews = memberViews.length > 0;
const location = useLocation();
const memberCount = useMemberCount();
const routing = useEmberRouting();
const commentModerationEnabled = useFeatureFlag('commentModeration');
const membersForwardEnabled = useFeatureFlag('membersForward');
const visibleMemberViews = membersForwardEnabled ? memberViews : [];
const hasMemberViews = visibleMemberViews.length > 0;

const showTags = currentUser && canManageTags(currentUser);
const showMembers = currentUser && canManageMembers(currentUser);
Expand All @@ -86,20 +84,14 @@ 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 hasActiveMemberChild = visibleMemberViews.some(view => view.isActive);
const membersExpanded = savedMembersExpanded;
const membersNavActive = isMembersNavActive({
membersForwardEnabled,
isOnMembersForward,
hasActiveMemberView,
isMembersExpanded: membersExpanded,
isLegacyMembersRouteActive: routing.isRouteActive(getMembersNavActiveRoutes())
});
const isMembersBaseRouteActive = routing.isRouteActive(['members', 'member', 'member.new', 'members-activity']);
const postsRoute = routing.getRouteUrl('posts');
const isPostsRouteActive = routing.isRouteActive('posts');
const postsNavActive = isPostsRouteActive || (!postsExpanded && hasActivePostChild);
const membersRoute = membersForwardEnabled ? 'members-forward' : routing.getRouteUrl('members');
const membersNavActive = (isMembersBaseRouteActive && !hasActiveMemberChild) || (!membersExpanded && hasActiveMemberChild);
const membersRoute = routing.getRouteUrl('members');

return (
<SidebarGroup {...props}>
Expand Down Expand Up @@ -176,7 +168,7 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {

{showMembers && (
<>
{membersForwardEnabled && hasMemberViews ? (
{hasMemberViews ? (
<NavMenuItem.Collapsible
expanded={membersExpanded}
id="members-submenu"
Expand Down
13 changes: 13 additions & 0 deletions apps/admin/src/members-route-gate.tsx
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 />;
}

return <Outlet />;
}
82 changes: 82 additions & 0 deletions apps/admin/src/members-route.test.tsx
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', '/');
});
});
22 changes: 22 additions & 0 deletions apps/admin/src/members-route.tsx
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 />;
}
33 changes: 32 additions & 1 deletion apps/admin/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Ember
import { EmberFallback, ForceUpgradeGuard } from "./ember-bridge";
import type { RouteHandle } from "./ember-bridge";
import { MembersRoute } from "./members-route";

import { NotFound } from "./not-found";

Expand All @@ -40,7 +41,8 @@
"/tags/new",
"/explore/*",
"/migrate/*",
"/members/*",
"/members/new",
"/members/:member_id",
"/members-activity",
"/designsandbox",
"/mentions",
Expand All @@ -53,6 +55,33 @@
Component: EmberFallback,
handle: emberFallbackHandle,
}));

const membersRoute: RouteObject = {
path: "/members",
element: <MembersRoute />,
handle: emberFallbackHandle,
children: [
{
index: true,
lazy: lazyComponent(() => import("@tryghost/posts/src/views/members/members"))
},
{
path: "import",
lazy: lazyComponent(() => import("@tryghost/posts/src/views/members/members"))
}
]
};

const membersForwardRedirectRoute: RouteObject = {
path: "/members-forward",
// TODO: Remove once the legacy Ember members list is deleted.

Check warning on line 77 in apps/admin/src/routes.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1If0oNrwhSJOCgFKsF&open=AZ1If0oNrwhSJOCgFKsF&pullRequest=27040
handle: emberFallbackHandle,
loader: ({request}) => {
const url = new URL(request.url);
return redirect(`/members${url.search}`);
}
};

export const routes: RouteObject[] = [
{
// ForceUpgradeGuard wraps all routes to redirect to /pro when in force upgrade mode.
Expand All @@ -69,6 +98,8 @@
Component: EmberFallback,
handle: emberFallbackHandle,
},
membersRoute,
membersForwardRedirectRoute,
{
element: (
<PostsAppContextProvider value={{ fromAnalytics: true }}>
Expand Down
Loading
Loading