Skip to content

Commit

Permalink
Newswires UI: add pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
sb-dev committed Jan 23, 2025
1 parent 4cb3429 commit 3fad1a0
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 34 deletions.
4 changes: 3 additions & 1 deletion newswires/client/src/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const Feed = () => {
titleSize="s"
/>
)}
{(status == 'success' || status == 'offline') &&
{(status == 'success' ||
status == 'offline' ||
status == 'loading-more') &&
queryData.results.length > 0 && (
<WireItemTable wires={queryData.results} />
)}
Expand Down
69 changes: 44 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,10 +14,12 @@ import {
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useEffect, useState } from 'react';

Check warning on line 17 in newswires/client/src/WireItemTable.tsx

View workflow job for this annotation

GitHub Actions / Build and upload to riffraff

'useEffect' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 17 in newswires/client/src/WireItemTable.tsx

View workflow job for this annotation

GitHub Actions / Build and upload to riffraff

'useState' is defined but never used. Allowed unused vars must match /^_/u
import sanitizeHtml from 'sanitize-html';
import { useSearch } from './context/SearchContext.tsx';
import { formatTimestamp } from './formatTimestamp';
import type { WireData } from './sharedTypes';
import { configToUrl } from './urlState.ts';

Check warning on line 22 in newswires/client/src/WireItemTable.tsx

View workflow job for this annotation

GitHub Actions / Build and upload to riffraff

'configToUrl' is defined but never used. Allowed unused vars must match /^_/u

const fadeOutBackground = css`
animation: fadeOut ease-out 15s;
Expand All @@ -31,38 +34,54 @@ const fadeOutBackground = css`
`;

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

const selectedWireId = config.itemId;

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={status === 'loading-more'}
css={css`
${euiScreenReaderOnly()}
margin-top: 12px;
`}
onClick={loadMoreResults}
>
<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>
{status === 'loading-more' ? 'Loading' : 'Load more'}
</EuiButton>
</>
);
};

Expand Down
42 changes: 40 additions & 2 deletions newswires/client/src/context/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ const StateSchema = z.discriminatedUnion('status', [
successfulQueryHistory: SearchHistorySchema,
autoUpdate: z.boolean().default(true),
}),
z.object({
status: z.literal('loading-more'),
error: z.string().optional(),
queryData: WiresQueryResponseSchema,
successfulQueryHistory: SearchHistorySchema,
autoUpdate: z.boolean().default(true),
}),
z.object({
status: z.literal('success'),
error: z.string().optional(),
Expand Down Expand Up @@ -72,11 +79,16 @@ export type State = z.infer<typeof StateSchema>;
// Action Schema
const ActionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('ENTER_QUERY') }),
z.object({ type: z.literal('LOAD_MORE_RESULTS') }),
z.object({
type: z.literal('FETCH_SUCCESS'),
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 +112,7 @@ export type SearchContextShape = {
handleNextItem: () => void;
handlePreviousItem: () => void;
toggleAutoUpdate: () => void;
loadMoreResults: () => void;
};
export const SearchContext: Context<SearchContextShape | null> =
createContext<SearchContextShape | null>(null);
Expand Down Expand Up @@ -167,9 +180,29 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
});
}

if (state.status === 'success' || state.status === 'offline') {
if (state.status === 'loading-more') {
fetchResults(
currentConfig.query,
undefined,
Math.min(...state.queryData.results.map((wire) => wire.id)).toString(),
)
.then((data) => {
dispatch({ type: 'APPEND_RESULTS', data });
})
.catch((error) => {
const errorMessage =
error instanceof Error ? error.message : 'unknown error';
dispatch({ type: 'FETCH_ERROR', error: errorMessage });
});
}

if (
state.status === 'success' ||
state.status === 'offline' ||
state.status === 'loading-more'
) {
pollingInterval = setInterval(() => {
if (state.autoUpdate) {
if (state.autoUpdate && state.status !== 'loading-more') {
fetchResults(
currentConfig.query,
Math.max(
Expand Down Expand Up @@ -273,6 +306,10 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
dispatch({ type: 'TOGGLE_AUTO_UPDATE' });
};

const loadMoreResults = () => {
dispatch({ type: 'LOAD_MORE_RESULTS' });
};

return (
<SearchContext.Provider
value={{
Expand All @@ -285,6 +322,7 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
handleNextItem,
handlePreviousItem,
toggleAutoUpdate,
loadMoreResults,
}}
>
{children}
Expand Down
43 changes: 43 additions & 0 deletions newswires/client/src/context/SearchReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ describe('SearchReducer', () => {
queryData: { results: [sampleWireData] },
};

const loadingMoreState: State = {
...initialState,
status: 'loading-more',
queryData: { results: [sampleWireData] },
};

const offlineState: State = {
status: 'offline',
queryData: { results: [sampleWireData] },
Expand Down Expand Up @@ -98,6 +104,31 @@ describe('SearchReducer', () => {
});
});

it(`should handle APPEND_RESULTS action in loading-more state`, () => {
const state: State = {
...loadingMoreState,
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 Expand Up @@ -136,4 +167,16 @@ describe('SearchReducer', () => {
expect(newState.status).toBe('loading');
});
});

[successState, offlineState].forEach((state) => {
it(`should handle LOAD_MORE_RESULTS action in ${state.status} state`, () => {
const action: Action = {
type: 'LOAD_MORE_RESULTS',
};

const newState = SearchReducer(state, action);

expect(newState.status).toBe('loading-more');
});
});
});
37 changes: 37 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,17 @@ export const SearchReducer = (state: State, action: Action): State => {
default:
return state;
}
case 'APPEND_RESULTS':
switch (state.status) {
case 'loading-more':
return {
...state,
status: 'success',
queryData: appendQueryData(state.queryData, action.data),
};
default:
return state;
}
case 'FETCH_ERROR':
switch (state.status) {
case 'loading':
Expand Down Expand Up @@ -117,6 +142,18 @@ export const SearchReducer = (state: State, action: Action): State => {
...state,
status: 'loading',
};
case 'LOAD_MORE_RESULTS':
switch (state.status) {
case 'success':
case 'offline':
return {
...state,
status: 'loading-more',
};
default:
return state;
}

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

export const paramsToQuerystring = (
config: Query,
sinceId: number | undefined = undefined,
sinceId: string | undefined = undefined,
beforeId: string | undefined = undefined,
): string => {
const params = Object.entries(config).reduce<Array<[string, string]>>(
(acc, [k, v]) => {
Expand All @@ -89,7 +90,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 3fad1a0

Please sign in to comment.