mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
Slightly improved AdminX pagination behaviour (#18331)
refs https://github.com/TryGhost/Product/issues/3832 --- ### <samp>🤖 Generated by Copilot at e1d84b3</samp> This pull request fixes pagination bugs and improves pagination features in various components and hooks of the admin settings app. It uses the `meta` object from the API responses to display and fetch the correct number of items in the lists of newsletters, tiers, users and actions. It also simplifies and refactors some of the code to avoid repetition and unnecessary properties.
This commit is contained in:
parent
0e35baaf01
commit
aa8063d081
@ -163,10 +163,10 @@ const Select: React.FC<SelectProps> = ({
|
||||
components: {DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator},
|
||||
inputId: id,
|
||||
isClearable: false,
|
||||
options: options,
|
||||
options,
|
||||
placeholder: prompt ? prompt : '',
|
||||
value: selectedOption,
|
||||
unstyled,
|
||||
unstyled: true,
|
||||
onChange: onSelect
|
||||
};
|
||||
|
||||
|
@ -74,10 +74,12 @@ export const useBrowseActions = createInfiniteQuery<ActionsResponseType>({
|
||||
}
|
||||
});
|
||||
|
||||
const meta = pages.at(-1)!.meta;
|
||||
|
||||
return {
|
||||
actions: actions.reverse(),
|
||||
meta: pages.at(-1)!.meta,
|
||||
isEnd: pages.at(-1)!.actions.length < pages.at(-1)!.meta.pagination.limit
|
||||
meta,
|
||||
isEnd: meta ? meta.pagination.pages === meta.pagination.page : true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ const dataType = 'NewslettersResponseType';
|
||||
export const useBrowseNewsletters = createInfiniteQuery<NewslettersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
path: '/newsletters/',
|
||||
defaultSearchParams: {include: 'count.active_members,count.posts', limit: '20'},
|
||||
defaultSearchParams: {include: 'count.active_members,count.posts', limit: '50'},
|
||||
defaultNextPageParams: (lastPage, otherParams) => ({
|
||||
...otherParams,
|
||||
page: (lastPage.meta?.pagination.next || 1).toString()
|
||||
@ -59,11 +59,12 @@ export const useBrowseNewsletters = createInfiniteQuery<NewslettersResponseType
|
||||
returnData: (originalData) => {
|
||||
const {pages} = originalData as InfiniteData<NewslettersResponseType>;
|
||||
const newsletters = pages.flatMap(page => page.newsletters);
|
||||
const meta = pages.at(-1)!.meta;
|
||||
|
||||
return {
|
||||
newsletters: newsletters,
|
||||
meta: pages.at(-1)!.meta,
|
||||
isEnd: pages.at(-1)!.newsletters.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||
meta,
|
||||
isEnd: meta ? meta.pagination.pages === meta.pagination.page : true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -34,7 +34,6 @@ const dataType = 'TiersResponseType';
|
||||
export const useBrowseTiers = createInfiniteQuery<TiersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
path: '/tiers/',
|
||||
defaultSearchParams: {limit: '20'},
|
||||
defaultNextPageParams: (lastPage, otherParams) => ({
|
||||
...otherParams,
|
||||
page: (lastPage.meta?.pagination.next || 1).toString()
|
||||
@ -42,11 +41,12 @@ export const useBrowseTiers = createInfiniteQuery<TiersResponseType & {isEnd: bo
|
||||
returnData: (originalData) => {
|
||||
const {pages} = originalData as InfiniteData<TiersResponseType>;
|
||||
const tiers = pages.flatMap(page => page.tiers);
|
||||
const meta = pages.at(-1)!.meta;
|
||||
|
||||
return {
|
||||
tiers,
|
||||
meta: pages.at(-1)!.meta,
|
||||
isEnd: pages.at(-1)!.tiers.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||
meta,
|
||||
isEnd: meta ? meta.pagination.pages === meta.pagination.page : true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -75,11 +75,12 @@ export const useBrowseUsers = createInfiniteQuery<UsersResponseType & {isEnd: bo
|
||||
returnData: (originalData) => {
|
||||
const {pages} = originalData as InfiniteData<UsersResponseType>;
|
||||
const users = pages.flatMap(page => page.users);
|
||||
const meta = pages.at(-1)!.meta;
|
||||
|
||||
return {
|
||||
users: users,
|
||||
meta: pages.at(-1)!.meta,
|
||||
isEnd: pages.at(-1)!.users.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||
meta,
|
||||
isEnd: meta ? meta.pagination.pages === meta.pagination.page : true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
updateRoute('newsletters/add');
|
||||
};
|
||||
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
||||
const {data: {newsletters, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
const {data: {newsletters, meta, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
|
||||
@ -43,7 +43,11 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
title='Newsletters'
|
||||
>
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||
{isEnd === false && <Button
|
||||
label={`Load more (showing ${newsletters?.length || 0}/${meta?.pagination.total || 0} newsletters)`}
|
||||
link
|
||||
onClick={() => fetchNextPage()}
|
||||
/>}
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -206,12 +206,16 @@ const InvitesUserList: React.FC<InviteListProps> = ({users}) => {
|
||||
|
||||
const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords, highlight = true}) => {
|
||||
const {
|
||||
totalUsers,
|
||||
users,
|
||||
ownerUser,
|
||||
adminUsers,
|
||||
editorUsers,
|
||||
authorUsers,
|
||||
contributorUsers,
|
||||
invites
|
||||
invites,
|
||||
hasNextPage,
|
||||
fetchNextPage
|
||||
} = useStaffUsers();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
@ -266,6 +270,11 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
|
||||
>
|
||||
<Owner user={ownerUser} />
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||
{hasNextPage && <Button
|
||||
label={`Load more (showing ${users.length}/${totalUsers} users)`}
|
||||
link
|
||||
onClick={() => fetchNextPage()}
|
||||
/>}
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ const StripeConnectedButton: React.FC<{className?: string; onClick: () => void;}
|
||||
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const [selectedTab, setSelectedTab] = useState('active-tiers');
|
||||
const {settings, config} = useGlobalData();
|
||||
const {data: {tiers, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||
const {data: {tiers, meta, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||
const activeTiers = getActiveTiers(tiers || []);
|
||||
const archivedTiers = getArchivedTiers(tiers || []);
|
||||
const {updateRoute} = useRouting();
|
||||
@ -81,7 +81,11 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
</div>
|
||||
|
||||
{content}
|
||||
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||
{isEnd === false && <Button
|
||||
label={`Load more (showing ${tiers?.length || 0}/${meta?.pagination.total || 0} tiers)`}
|
||||
link
|
||||
onClick={() => fetchNextPage()}
|
||||
/>}
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import {useGlobalData} from '../components/providers/GlobalDataProvider';
|
||||
import {useMemo} from 'react';
|
||||
|
||||
export type UsersHook = {
|
||||
totalUsers: number;
|
||||
users: User[];
|
||||
invites: UserInvite[];
|
||||
ownerUser: User;
|
||||
@ -32,7 +33,7 @@ function getOwnerUser(users: User[]): User {
|
||||
|
||||
const useStaffUsers = (): UsersHook => {
|
||||
const {currentUser} = useGlobalData();
|
||||
const {data: {users, isEnd} = {users: []}, isLoading: usersLoading, fetchNextPage} = useBrowseUsers();
|
||||
const {data: {users, meta, isEnd} = {users: []}, isLoading: usersLoading, fetchNextPage} = useBrowseUsers();
|
||||
const {data: {invites} = {invites: []}, isLoading: invitesLoading} = useBrowseInvites();
|
||||
const {data: {roles} = {}, isLoading: rolesLoading} = useBrowseRoles();
|
||||
|
||||
@ -52,6 +53,7 @@ const useStaffUsers = (): UsersHook => {
|
||||
}), [invites, roles]);
|
||||
|
||||
return {
|
||||
totalUsers: meta?.pagination.total || 0,
|
||||
users,
|
||||
ownerUser,
|
||||
adminUsers,
|
||||
@ -61,7 +63,7 @@ const useStaffUsers = (): UsersHook => {
|
||||
currentUser,
|
||||
invites: mappedInvites,
|
||||
isLoading: usersLoading || invitesLoading || rolesLoading,
|
||||
hasNextPage: isEnd,
|
||||
hasNextPage: isEnd === false,
|
||||
fetchNextPage
|
||||
};
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports creating a new newsletter', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||
addNewsletter: {method: 'POST', path: '/newsletters/?opt_in_existing=true&include=count.active_members%2Ccount.posts', response: {newsletters: [{
|
||||
id: 'new-newsletter',
|
||||
name: 'New newsletter',
|
||||
@ -48,7 +48,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports updating a newsletter', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||
newsletters: [{
|
||||
...responseFixtures.newsletters.newsletters[0],
|
||||
@ -93,7 +93,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Displays a prompt when email verification is required', async ({page}) => {
|
||||
await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
||||
meta: {
|
||||
@ -120,7 +120,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports archiving newsletters', async ({page}) => {
|
||||
const activate = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[1].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||
newsletters: [{
|
||||
...responseFixtures.newsletters.newsletters[1],
|
||||
@ -157,7 +157,7 @@ test.describe('Newsletter settings', async () => {
|
||||
|
||||
const archive = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||
newsletters: [{
|
||||
...responseFixtures.newsletters.newsletters[0],
|
||||
@ -206,7 +206,7 @@ test.describe('Newsletter settings', async () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters}
|
||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
@ -46,7 +46,7 @@ test.describe('Access settings', async () => {
|
||||
test('Supports selecting specific tiers', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||
browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers},
|
||||
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
||||
{key: 'default_content_visibility', value: 'tiers'},
|
||||
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.map(tier => tier.id))}
|
||||
|
@ -13,7 +13,7 @@ test.describe('Tier settings', async () => {
|
||||
await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers}
|
||||
browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
@ -55,7 +55,7 @@ test.describe('Tier settings', async () => {
|
||||
...globalDataRequests,
|
||||
addTier: {method: 'POST', path: '/tiers/', response: {tiers: [newTier]}},
|
||||
// This request will be reloaded after the new tier is added
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: {tiers: [...responseFixtures.tiers.tiers, newTier]}}
|
||||
browseTiers: {method: 'GET', path: '/tiers/', response: {tiers: [...responseFixtures.tiers.tiers, newTier]}}
|
||||
}});
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
@ -77,7 +77,7 @@ test.describe('Tier settings', async () => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||
browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers},
|
||||
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[1].id}/`, response: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[1],
|
||||
@ -140,7 +140,7 @@ test.describe('Tier settings', async () => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||
browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers},
|
||||
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[0].id}/`, response: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[0],
|
||||
|
Loading…
Reference in New Issue
Block a user