Added show all recommendations instead of pagination (#18276)

fixes https://github.com/TryGhost/Product/issues/3923
This commit is contained in:
Simon Backx 2023-09-21 17:49:41 +02:00 committed by GitHub
parent 65c4553467
commit 96ecc73b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 15 deletions

View File

@ -8,6 +8,11 @@ import clsx from 'clsx';
import {LoadingIndicator} from './LoadingIndicator'; import {LoadingIndicator} from './LoadingIndicator';
import {PaginationData} from '../../hooks/usePagination'; import {PaginationData} from '../../hooks/usePagination';
export interface ShowMoreData {
hasMore: boolean;
loadMore: () => void;
}
interface TableProps { interface TableProps {
/** /**
* If the table is the primary content on a page (e.g. Members table) then you can set a pagetitle to be consistent * If the table is the primary content on a page (e.g. Members table) then you can set a pagetitle to be consistent
@ -21,6 +26,7 @@ interface TableProps {
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
pagination?: PaginationData; pagination?: PaginationData;
showMore?: ShowMoreData;
} }
const OptionalPagination = ({pagination}: {pagination?: PaginationData}) => { const OptionalPagination = ({pagination}: {pagination?: PaginationData}) => {
@ -31,7 +37,19 @@ const OptionalPagination = ({pagination}: {pagination?: PaginationData}) => {
return <Pagination {...pagination}/>; return <Pagination {...pagination}/>;
}; };
const Table: React.FC<TableProps> = ({header, children, borderTop, hint, hintSeparator, pageTitle, className, pagination, isLoading}) => { const OptionalShowMore = ({showMore}: {showMore?: ShowMoreData}) => {
if (!showMore || !showMore.hasMore) {
return null;
}
return (
<div className={`mt-1 flex items-center gap-2 text-xs text-grey-700`}>
<button type='button' onClick={showMore.loadMore}>Show all</button>
</div>
);
};
const Table: React.FC<TableProps> = ({header, children, borderTop, hint, hintSeparator, pageTitle, className, pagination, showMore, isLoading}) => {
const tableClasses = clsx( const tableClasses = clsx(
(borderTop || pageTitle) && 'border-t border-grey-300', (borderTop || pageTitle) && 'border-t border-grey-300',
'w-full', 'w-full',
@ -109,12 +127,13 @@ const Table: React.FC<TableProps> = ({header, children, borderTop, hint, hintSep
{isLoading && <LoadingIndicator delay={200} size='lg' style={loadingStyle} />} {isLoading && <LoadingIndicator delay={200} size='lg' style={loadingStyle} />}
{(hint || pagination) && {(hint || pagination || showMore) &&
<div className="-mt-px"> <div className="-mt-px">
{(hintSeparator || pagination) && <Separator />} {(hintSeparator || pagination) && <Separator />}
<div className="flex flex-col-reverse items-start justify-between gap-1 pt-2 md:flex-row md:items-center md:gap-0 md:pt-0"> <div className="flex flex-col-reverse items-start justify-between gap-1 pt-2 md:flex-row md:items-center md:gap-0 md:pt-0">
<Hint>{hint ?? ' '}</Hint> <Hint>{hint ?? ' '}</Hint>
<OptionalPagination pagination={pagination} /> <OptionalPagination pagination={pagination} />
<OptionalShowMore showMore={showMore} />
</div> </div>
</div>} </div>}
</div> </div>

View File

@ -1,4 +1,5 @@
import {Meta, apiUrl, createMutation, createPaginatedQuery, useFetchApi} from '../utils/apiRequests'; import {InfiniteData} from '@tanstack/react-query';
import {Meta, apiUrl, createInfiniteQuery, createMutation, useFetchApi} from '../utils/apiRequests';
export type Recommendation = { export type Recommendation = {
id: string id: string
@ -28,10 +29,23 @@ export interface RecommendationDeleteResponseType {}
const dataType = 'RecommendationResponseType'; const dataType = 'RecommendationResponseType';
export const useBrowseRecommendations = createPaginatedQuery<RecommendationResponseType>({ export const useBrowseRecommendations = createInfiniteQuery<RecommendationResponseType>({
dataType, dataType,
path: '/recommendations/', path: '/recommendations/',
defaultSearchParams: {} returnData: (originalData) => {
const {pages} = originalData as InfiniteData<RecommendationResponseType>;
let recommendations = pages.flatMap(page => page.recommendations);
// Remove duplicates
recommendations = recommendations.filter((recommendation, index) => {
return recommendations.findIndex(({id}) => id === recommendation.id) === index;
});
return {
recommendations,
meta: pages[pages.length - 1].meta
};
}
}); });
export const useDeleteRecommendation = createMutation<RecommendationDeleteResponseType, Recommendation>({ export const useDeleteRecommendation = createMutation<RecommendationDeleteResponseType, Recommendation>({

View File

@ -7,6 +7,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView'; import TabView from '../../../admin-x-ds/global/TabView';
import useRouting from '../../../hooks/useRouting'; import useRouting from '../../../hooks/useRouting';
import useSettingGroup from '../../../hooks/useSettingGroup'; import useSettingGroup from '../../../hooks/useSettingGroup';
import {ShowMoreData} from '../../../admin-x-ds/global/Table';
import {useBrowseRecommendations} from '../../../api/recommendations'; import {useBrowseRecommendations} from '../../../api/recommendations';
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary'; import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
@ -17,11 +18,37 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
handleSave handleSave
} = useSettingGroup(); } = useSettingGroup();
const {pagination, data: {recommendations} = {}, isLoading} = useBrowseRecommendations({ const {data: {recommendations, meta} = {}, isLoading, hasNextPage, fetchNextPage} = useBrowseRecommendations({
searchParams: { searchParams: {
include: 'count.clicks,count.subscribers' include: 'count.clicks,count.subscribers',
limit: '5'
},
// We first load 5, then load 100 at a time (= show all, but without using the dangerous 'all' limit)
getNextPageParams: (lastPage, otherParams) => {
if (!lastPage.meta) {
return;
} }
const {limit, page, pages} = lastPage.meta.pagination;
if (page >= pages) {
return;
}
const newPage = limit < 100 ? 1 : (page + 1);
return {
...otherParams,
page: newPage.toString(),
limit: '100'
};
},
keepPreviousData: true
}); });
const showMore: ShowMoreData = {
hasMore: !!hasNextPage,
loadMore: fetchNextPage
};
const [selectedTab, setSelectedTab] = useState('your-recommendations'); const [selectedTab, setSelectedTab] = useState('your-recommendations');
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
@ -41,7 +68,7 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
{ {
id: 'your-recommendations', id: 'your-recommendations',
title: 'Your recommendations', title: 'Your recommendations',
contents: (<RecommendationList isLoading={isLoading} pagination={pagination} recommendations={recommendations ?? []}/>) contents: (<RecommendationList isLoading={isLoading} recommendations={recommendations ?? []} showMore={showMore}/>)
}, },
{ {
id: 'recommending-you', id: 'recommending-you',
@ -51,7 +78,7 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
]; ];
const groupDescription = ( const groupDescription = (
<>Share favorite sites with your audience after they subscribe. {(pagination && pagination.total && pagination.total > 0) && <Link href={recommendationsURL} target='_blank'>Preview</Link>}</> <>Share favorite sites with your audience after they subscribe. {(!!meta && !!meta.pagination.total && meta.pagination.total > 0) && <Link href={recommendationsURL} target='_blank'>Preview</Link>}</>
); );
return ( return (

View File

@ -1,7 +1,7 @@
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel'; import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
import React from 'react'; import React from 'react';
import RecommendationIcon from './RecommendationIcon'; import RecommendationIcon from './RecommendationIcon';
import Table from '../../../../admin-x-ds/global/Table'; import Table, {ShowMoreData} from '../../../../admin-x-ds/global/Table';
import TableCell from '../../../../admin-x-ds/global/TableCell'; import TableCell from '../../../../admin-x-ds/global/TableCell';
// import TableHead from '../../../../admin-x-ds/global/TableHead'; // import TableHead from '../../../../admin-x-ds/global/TableHead';
import EditRecommendationModal from './EditRecommendationModal'; import EditRecommendationModal from './EditRecommendationModal';
@ -13,7 +13,8 @@ import {Recommendation} from '../../../../api/recommendations';
interface RecommendationListProps { interface RecommendationListProps {
recommendations: Recommendation[], recommendations: Recommendation[],
pagination: PaginationData, pagination?: PaginationData,
showMore?: ShowMoreData,
isLoading: boolean isLoading: boolean
} }
@ -64,9 +65,9 @@ const RecommendationItem: React.FC<{recommendation: Recommendation}> = ({recomme
// TODO: Remove if we decide we don't need headers // TODO: Remove if we decide we don't need headers
// const tableHeader = (<><TableHead>Site</TableHead><TableHead>Conversions from you</TableHead></>); // const tableHeader = (<><TableHead>Site</TableHead><TableHead>Conversions from you</TableHead></>);
const RecommendationList: React.FC<RecommendationListProps> = ({recommendations, pagination, isLoading}) => { const RecommendationList: React.FC<RecommendationListProps> = ({recommendations, pagination, showMore, isLoading}) => {
if (isLoading || recommendations.length) { if (isLoading || recommendations.length) {
return <Table hint={<span>Shown randomized to your readers</span>} isLoading={isLoading} pagination={pagination} hintSeparator> return <Table hint={<span>Shown randomized to your readers</span>} isLoading={isLoading} pagination={pagination} showMore={showMore} hintSeparator>
{recommendations && recommendations.map(recommendation => <RecommendationItem key={recommendation.id} recommendation={recommendation} />)} {recommendations && recommendations.map(recommendation => <RecommendationItem key={recommendation.id} recommendation={recommendation} />)}
</Table>; </Table>;
} else { } else {

View File

@ -154,7 +154,7 @@ type InfiniteQueryOptions<ResponseData> = Omit<QueryOptions<ResponseData>, 'retu
type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & { type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & {
searchParams?: Record<string, string>; searchParams?: Record<string, string>;
getNextPageParams: (data: ResponseData, params: Record<string, string>) => Record<string, string>; getNextPageParams: (data: ResponseData, params: Record<string, string>) => Record<string, string>|undefined;
}; };
export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<ResponseData>) => ({searchParams, getNextPageParams, ...query}: InfiniteQueryHookOptions<ResponseData>) => { export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<ResponseData>) => ({searchParams, getNextPageParams, ...query}: InfiniteQueryHookOptions<ResponseData>) => {