mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03:00
Added show all recommendations instead of pagination (#18276)
fixes https://github.com/TryGhost/Product/issues/3923
This commit is contained in:
parent
65c4553467
commit
96ecc73b17
@ -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>
|
||||||
|
@ -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>({
|
||||||
|
@ -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 (
|
||||||
|
@ -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 {
|
||||||
|
@ -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>) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user