Skip to content

Commit 87f82a7

Browse files
author
Deepak kudi
committed
fix(members): include site title in export filename
1 parent 5edf3dd commit 87f82a7

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

apps/posts/src/views/members/components/members-actions.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,39 @@ interface MembersActionsProps {
1616
memberCount: number;
1717
nql?: string;
1818
search: string;
19+
siteTitle?: string | null;
1920
canBulkDelete: boolean;
2021
onImportComplete?: (importResponse?: ImportResponse) => void;
2122
}
2223

23-
async function exportMembers(filter?: string, search?: string): Promise<void> {
24+
export function getMembersExportFileName(siteTitle?: string | null): string {
25+
const titleSlug = siteTitle
26+
?.toLowerCase()
27+
.replace(/[^a-z0-9]+/g, '-')
28+
.replace(/^-|-$/g, '');
29+
const titlePrefix = titleSlug ? `${titleSlug}.` : '';
30+
const datetime = new Date().toJSON().substring(0, 10);
31+
32+
return `${titlePrefix}ghost.members.${datetime}.csv`;
33+
}
34+
35+
export async function exportMembers(filter?: string, search?: string, siteTitle?: string | null): Promise<void> {
2436
const params = new URLSearchParams({limit: 'all'});
2537
if (filter) {
2638
params.set('filter', filter);
2739
}
2840
if (search) {
2941
params.set('search', search);
3042
}
31-
const datetime = new Date().toJSON().substring(0, 10);
32-
await blobDownloadFromEndpoint(`/members/upload/?${params}`, `members.${datetime}.csv`);
43+
await blobDownloadFromEndpoint(`/members/upload/?${params}`, getMembersExportFileName(siteTitle));
3344
}
3445

3546
const MembersActions: React.FC<MembersActionsProps> = ({
3647
hasFilterOrSearch,
3748
memberCount,
3849
nql,
3950
search,
51+
siteTitle,
4052
canBulkDelete,
4153
onImportComplete
4254
}) => {
@@ -62,14 +74,14 @@ const MembersActions: React.FC<MembersActionsProps> = ({
6274
const memberOperationParams = buildMemberOperationParams({nql, search});
6375
const handleExport = useCallback(async () => {
6476
try {
65-
await exportMembers(nql, search);
77+
await exportMembers(nql, search, siteTitle);
6678
} catch (e) {
6779
toast.error('Export failed', {
6880
description: 'There was a problem downloading your member data. Please check your connection and try again.'
6981
});
7082
throw e;
7183
}
72-
}, [nql, search]);
84+
}, [nql, search, siteTitle]);
7385

7486
const handleAddLabel = useCallback(async (labelIds: string[]) => {
7587
try {

apps/posts/src/views/members/members.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {useLocation, useSearchParams} from 'react-router';
2424
const SEARCH_DEBOUNCE_MS = 250;
2525
const MEMBERS_HELP_CARDS_LIMIT = 6;
2626

27-
const MembersPage: React.FC<{timezone: string; membershipsEnabled: boolean}> = ({timezone, membershipsEnabled}) => {
27+
const MembersPage: React.FC<{timezone: string; membershipsEnabled: boolean; siteTitle?: string | null}> = ({timezone, membershipsEnabled, siteTitle}) => {
2828
const headerRef = useRef<HTMLDivElement | null>(null);
2929
const setHeaderContentRef = useCallback((node: HTMLDivElement | null) => {
3030
headerRef.current = node?.closest('[data-list-page="header"]') as HTMLDivElement | null;
@@ -155,6 +155,7 @@ const MembersPage: React.FC<{timezone: string; membershipsEnabled: boolean}> = (
155155
memberCount={totalMembers}
156156
nql={nql}
157157
search={search}
158+
siteTitle={siteTitle}
158159
onImportComplete={() => {
159160
void refetch();
160161
}}
@@ -282,9 +283,10 @@ const Members: React.FC = () => {
282283

283284
const timezone = getSiteTimezone(settingsData.settings);
284285
const membersSignupAccess = getSettingValue<string>(settingsData.settings, 'members_signup_access');
286+
const siteTitle = getSettingValue<string>(settingsData.settings, 'title');
285287
const membershipsEnabled = membersSignupAccess !== 'none';
286288

287-
return <MembersPage membershipsEnabled={membershipsEnabled} timezone={timezone} />;
289+
return <MembersPage membershipsEnabled={membershipsEnabled} siteTitle={siteTitle} timezone={timezone} />;
288290
};
289291

290292
export default Members;

apps/posts/test/unit/views/members/members-actions.test.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import MembersActions from '@src/views/members/components/members-actions';
1+
import MembersActions, {exportMembers, getMembersExportFileName} from '@src/views/members/components/members-actions';
22
import React from 'react';
3-
import {beforeEach, describe, expect, it, vi} from 'vitest';
3+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
44
import {render, screen} from '@testing-library/react';
55

66
const importModalPropsRef: {current: Record<string, unknown> | null} = {current: null};
7-
const {mockUseLocation, mockUseNavigate} = vi.hoisted(() => ({
7+
const {mockBlobDownloadFromEndpoint, mockUseLocation, mockUseNavigate} = vi.hoisted(() => ({
8+
mockBlobDownloadFromEndpoint: vi.fn(),
89
mockUseLocation: vi.fn(),
910
mockUseNavigate: vi.fn()
1011
}));
@@ -14,6 +15,10 @@ vi.mock('@tryghost/admin-x-framework', () => ({
1415
useNavigate: mockUseNavigate
1516
}));
1617

18+
vi.mock('@tryghost/admin-x-framework/helpers', () => ({
19+
blobDownloadFromEndpoint: mockBlobDownloadFromEndpoint
20+
}));
21+
1722
vi.mock('@src/views/members/components/bulk-action-modals', () => ({
1823
ImportMembersModal: (props: Record<string, unknown>) => {
1924
importModalPropsRef.current = props;
@@ -64,12 +69,43 @@ const renderMembersActions = (props: Partial<React.ComponentProps<typeof Members
6469
};
6570

6671
describe('MembersActions', () => {
72+
afterEach(() => {
73+
vi.useRealTimers();
74+
});
75+
6776
beforeEach(() => {
6877
importModalPropsRef.current = null;
78+
mockBlobDownloadFromEndpoint.mockReset();
6979
setLocation('/members');
7080
mockUseNavigate.mockReturnValue(vi.fn());
7181
});
7282

83+
it('builds a members export filename with the slugified site title', () => {
84+
vi.useFakeTimers();
85+
vi.setSystemTime(new Date('2026-06-02T12:00:00.000Z'));
86+
87+
expect(getMembersExportFileName('My Publication')).toBe('my-publication.ghost.members.2026-06-02.csv');
88+
});
89+
90+
it('falls back to ghost when the members export site title is missing', () => {
91+
vi.useFakeTimers();
92+
vi.setSystemTime(new Date('2026-06-02T12:00:00.000Z'));
93+
94+
expect(getMembersExportFileName(null)).toBe('ghost.members.2026-06-02.csv');
95+
});
96+
97+
it('exports members with the site title in the CSV filename', async () => {
98+
vi.useFakeTimers();
99+
vi.setSystemTime(new Date('2026-06-02T12:00:00.000Z'));
100+
101+
await exportMembers(undefined, '', 'My Publication');
102+
103+
expect(mockBlobDownloadFromEndpoint).toHaveBeenCalledWith(
104+
'/members/upload/?limit=all',
105+
'my-publication.ghost.members.2026-06-02.csv'
106+
);
107+
});
108+
73109
it('opens the import modal on the import route', () => {
74110
setLocation('/members/import');
75111

0 commit comments

Comments
 (0)