Skip to content

Commit

Permalink
Merge pull request #108 from guardian/ui-add-pagination
Browse files Browse the repository at this point in the history
Newswires UI: add pagination
  • Loading branch information
sb-dev authored Jan 24, 2025
2 parents 11ac97d + 0fe356d commit a4f6a28
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 39 deletions.
78 changes: 53 additions & 25 deletions newswires/client/src/WireItemTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
EuiButton,
EuiFlexGroup,
euiScreenReaderOnly,
EuiTable,
Expand All @@ -13,6 +14,7 @@ import {
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useState } from 'react';
import sanitizeHtml from 'sanitize-html';
import { useSearch } from './context/SearchContext.tsx';
import { formatTimestamp } from './formatTimestamp';
Expand All @@ -31,38 +33,63 @@ const fadeOutBackground = css`
`;

export const WireItemTable = ({ wires }: { wires: WireData[] }) => {
const { config, handleSelectItem } = useSearch();
const { config, handleSelectItem, loadMoreResults } = useSearch();

const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);

const selectedWireId = config.itemId;

const handleLoadMoreResults = () => {
if (wires.length > 0) {
setIsLoadingMore(true);

const beforeId = Math.min(...wires.map((wire) => wire.id)).toString();

void loadMoreResults(beforeId).finally(() => {
setIsLoadingMore(false);
});
}
};

return (
<EuiTable
tableLayout="auto"
responsiveBreakpoint={config.view === 'item' ? true : 'm'}
>
<EuiTableHeader
<>
<EuiTable
tableLayout="auto"
responsiveBreakpoint={config.view === 'item' ? true : 'm'}
>
<EuiTableHeader
css={css`
${euiScreenReaderOnly()}
`}
>
<EuiTableHeaderCell>Headline</EuiTableHeaderCell>
<EuiTableHeaderCell>Version Created</EuiTableHeaderCell>
</EuiTableHeader>
<EuiTableBody>
{wires.map(({ id, supplier, content, isFromRefresh, highlight }) => (
<WireDataRow
key={id}
id={id}
supplier={supplier}
content={content}
isFromRefresh={isFromRefresh}
highlight={highlight}
selected={selectedWireId == id.toString()}
handleSelect={handleSelectItem}
/>
))}
</EuiTableBody>
</EuiTable>
<EuiButton
isLoading={isLoadingMore}
css={css`
${euiScreenReaderOnly()}
margin-top: 12px;
`}
onClick={handleLoadMoreResults}
>
<EuiTableHeaderCell>Headline</EuiTableHeaderCell>
<EuiTableHeaderCell>Version Created</EuiTableHeaderCell>
</EuiTableHeader>
<EuiTableBody>
{wires.map(({ id, supplier, content, isFromRefresh, highlight }) => (
<WireDataRow
key={id}
id={id}
supplier={supplier}
content={content}
isFromRefresh={isFromRefresh}
highlight={highlight}
selected={selectedWireId == id.toString()}
handleSelect={handleSelectItem}
/>
))}
</EuiTableBody>
</EuiTable>
{isLoadingMore ? 'Loading' : 'Load more'}
</EuiButton>
</>
);
};

Expand Down Expand Up @@ -101,6 +128,7 @@ const WireDataRow = ({
background-color: ${primaryBgColor};
border-left: 4px solid ${theme.euiTheme.colors.accent};
}
border-left: 4px solid
${selected ? theme.euiTheme.colors.primary : 'transparent'};
${isFromRefresh ? fadeOutBackground : ''}
Expand Down
20 changes: 19 additions & 1 deletion newswires/client/src/context/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const ActionSchema = z.discriminatedUnion('type', [
query: QuerySchema,
data: WiresQueryResponseSchema,
}),
z.object({
type: z.literal('APPEND_RESULTS'),
data: WiresQueryResponseSchema,
}),
z.object({ type: z.literal('FETCH_ERROR'), error: z.string() }),
z.object({ type: z.literal('RETRY') }),
z.object({ type: z.literal('SELECT_ITEM'), item: z.string().optional() }),
Expand All @@ -100,6 +104,7 @@ export type SearchContextShape = {
handleNextItem: () => void;
handlePreviousItem: () => void;
toggleAutoUpdate: () => void;
loadMoreResults: (beforeId: string) => Promise<void>;
};
export const SearchContext: Context<SearchContextShape | null> =
createContext<SearchContextShape | null>(null);
Expand Down Expand Up @@ -176,7 +181,7 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
...state.queryData.results.map((wire) => wire.id),
).toString()
: undefined;
fetchResults(currentConfig.query, sinceId)
fetchResults(currentConfig.query, { sinceId })
.then((data) => {
dispatch({ type: 'UPDATE_RESULTS', data });
})
Expand Down Expand Up @@ -274,6 +279,18 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
dispatch({ type: 'TOGGLE_AUTO_UPDATE' });
};

const loadMoreResults = async (beforeId: string): Promise<void> => {
return fetchResults(currentConfig.query, { beforeId })
.then((data) => {
dispatch({ type: 'APPEND_RESULTS', data });
})
.catch((error) => {
const errorMessage =
error instanceof Error ? error.message : 'unknown error';
dispatch({ type: 'FETCH_ERROR', error: errorMessage });
});
};

return (
<SearchContext.Provider
value={{
Expand All @@ -286,6 +303,7 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
handleNextItem,
handlePreviousItem,
toggleAutoUpdate,
loadMoreResults,
}}
>
{children}
Expand Down
25 changes: 25 additions & 0 deletions newswires/client/src/context/SearchReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,31 @@ describe('SearchReducer', () => {
});
});

it(`should handle APPEND_RESULTS action in success state`, () => {
const state: State = {
...successState,
queryData: { results: [{ ...sampleWireData, id: 2 }] },
};

const action: Action = {
type: 'APPEND_RESULTS',
data: { results: [{ ...sampleWireData, id: 1 }] },
};

const newState = SearchReducer(state, action);

expect(newState.status).toBe('success');
expect(newState.queryData?.results).toHaveLength(2);
expect(newState.queryData?.results).toContainEqual({
...sampleWireData,
id: 1,
});
expect(newState.queryData?.results).toContainEqual({
...sampleWireData,
id: 2,
});
});

[
{ state: initialState, expectedStatus: 'error' },
{ state: successState, expectedStatus: 'offline' },
Expand Down
20 changes: 20 additions & 0 deletions newswires/client/src/context/SearchReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ function mergeQueryData(
}
}

function appendQueryData(
existing: WiresQueryResponse | undefined,
newData: WiresQueryResponse,
): WiresQueryResponse {
if (existing) {
return {
...newData,
results: [...existing.results, ...newData.results],
};
} else {
return newData;
}
}

function getUpdatedHistory(
previousHistory: SearchHistory,
newQuery: Query,
Expand Down Expand Up @@ -85,6 +99,12 @@ export const SearchReducer = (state: State, action: Action): State => {
default:
return state;
}
case 'APPEND_RESULTS':
return {
...state,
status: 'success',
queryData: appendQueryData(state.queryData, action.data),
};
case 'FETCH_ERROR':
switch (state.status) {
case 'loading':
Expand Down
26 changes: 20 additions & 6 deletions newswires/client/src/context/fetchResults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('fetchResults', () => {
const mockQuery = { q: 'value' };
await fetchResults(mockQuery);

expect(paramsToQuerystring).toHaveBeenCalledWith(mockQuery);
expect(paramsToQuerystring).toHaveBeenCalledWith(mockQuery, {});
expect(pandaFetch).toHaveBeenCalledWith('/api/search?queryString', {
headers: { Accept: 'application/json' },
});
Expand Down Expand Up @@ -70,11 +70,25 @@ describe('fetchResults', () => {

it('should append sinceId to the query if provided', async () => {
const mockQuery = { q: 'value' };
await fetchResults(mockQuery, '123');
await fetchResults(mockQuery, { sinceId: '123' });

expect(paramsToQuerystring).toHaveBeenCalledWith({
...mockQuery,
sinceId: '123',
});
expect(paramsToQuerystring).toHaveBeenCalledWith(
{
...mockQuery,
},
{ sinceId: '123' },
);
});

it('should append beforeId to the query if provided', async () => {
const mockQuery = { q: 'value' };
await fetchResults(mockQuery, { beforeId: '123' });

expect(paramsToQuerystring).toHaveBeenCalledWith(
{
...mockQuery,
},
{ beforeId: '123' },
);
});
});
10 changes: 5 additions & 5 deletions newswires/client/src/context/fetchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { paramsToQuerystring } from '../urlState.ts';

export const fetchResults = async (
query: Query,
sinceId: string | undefined = undefined,
additionalParams: {
sinceId?: string;
beforeId?: string;
} = {},
): Promise<WiresQueryResponse> => {
const queryToSerialise = sinceId
? { ...query, sinceId: sinceId.toString() }
: query;
const queryString = paramsToQuerystring(queryToSerialise);
const queryString = paramsToQuerystring(query, additionalParams);
const response = await pandaFetch(`/api/search${queryString}`, {
headers: {
Accept: 'application/json',
Expand Down
13 changes: 11 additions & 2 deletions newswires/client/src/urlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ export const configToUrl = (config: Config): string => {

export const paramsToQuerystring = (
config: Query,
sinceId: number | undefined = undefined,
{
sinceId,
beforeId,
}: {
sinceId?: string;
beforeId?: string;
} = {},
): string => {
const params = Object.entries(config).reduce<Array<[string, string]>>(
(acc, [k, v]) => {
Expand All @@ -89,7 +95,10 @@ export const paramsToQuerystring = (
[],
);
if (sinceId !== undefined) {
params.push(['sinceId', sinceId.toString()]);
params.push(['sinceId', sinceId]);
}
if (beforeId !== undefined) {
params.push(['beforeId', beforeId]);
}
const querystring = new URLSearchParams(params).toString();
return querystring.length !== 0 ? `?${querystring}` : '';
Expand Down

0 comments on commit a4f6a28

Please sign in to comment.