Skip to content

Commit d7bdf52

Browse files
authored
Merge pull request #28 from blue-core-lod/result-pagination
Supports pagination for search results
2 parents 643530f + 05624cd commit d7bdf52

File tree

7 files changed

+128
-29
lines changed

7 files changed

+128
-29
lines changed

src/actionCreators/search.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const fetchSinopiaSearchResults =
2929
)
3030
)
3131
dispatch(addApiSearchHistory(sinopiaSearchUri, query, keycloak))
32+
// Use extracted options from response if available, otherwise use passed options
33+
const finalOptions = response.options || options
3234
dispatch(
3335
setSearchResults(
3436
"resource",
@@ -37,8 +39,9 @@ export const fetchSinopiaSearchResults =
3739
response.totalHits,
3840
facetResponse || {},
3941
query,
40-
options,
41-
response.error
42+
finalOptions,
43+
response.error,
44+
response.links
4245
)
4346
)
4447
if (response.results) {

src/actions/search.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export const setSearchResults = (
1111
facetResults,
1212
query,
1313
options,
14-
error
14+
error,
15+
links
1516
) => ({
1617
type: "SET_SEARCH_RESULTS",
1718
payload: {
@@ -23,6 +24,7 @@ export const setSearchResults = (
2324
query,
2425
options,
2526
error,
27+
links,
2628
},
2729
})
2830

src/components/search/Search.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
selectSearchUri,
1414
selectSearchOptions,
1515
selectSearchTotalResults,
16+
selectSearchLinks,
1617
} from "selectors/search"
1718
import { sinopiaSearchUri } from "utilities/authorityConfig"
1819
import useSearch from "hooks/useSearch"
@@ -38,9 +39,10 @@ const Search = (props) => {
3839
const totalResults = useSelector((state) =>
3940
selectSearchTotalResults(state, "resource")
4041
)
42+
const links = useSelector((state) => selectSearchLinks(state, "resource"))
4143

42-
const changeSearchPage = (startOfRange) => {
43-
fetchSearchResults(queryString, uri, searchOptions, startOfRange, keycloak)
44+
const changeSearchPage = (linkUrl) => {
45+
fetchSearchResults(linkUrl, uri, searchOptions, null, keycloak)
4446
}
4547

4648
return (
@@ -59,6 +61,7 @@ const Search = (props) => {
5961
resultsPerPage={searchOptions.resultsPerPage}
6062
startOfRange={searchOptions.startOfRange}
6163
totalResults={totalResults}
64+
links={links}
6265
changePage={changeSearchPage}
6366
/>
6467
<SearchResultsMessage />

src/components/search/SearchResultsPaging.jsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,36 @@ const SearchResultsPaging = (props) => {
88

99
const changePage = (event, page) => {
1010
event.preventDefault()
11+
const lastPage = Math.ceil(props.totalResults / props.resultsPerPage)
12+
13+
// If we have links and it's a next/previous action, use the link URL
14+
if (props.links) {
15+
if (page === currentPage + 1) {
16+
if (props.links.next) {
17+
props.changePage(props.links.next)
18+
}
19+
return
20+
}
21+
if (page === currentPage - 1) {
22+
if (props.links.prev) {
23+
props.changePage(props.links.prev)
24+
}
25+
return
26+
}
27+
if (page === 1) {
28+
if (props.links.first) {
29+
props.changePage(props.links.first)
30+
}
31+
return
32+
}
33+
if (page === lastPage) {
34+
if (props.links.last) {
35+
props.changePage(props.links.last)
36+
}
37+
return
38+
}
39+
}
40+
// Fallback to offset-based pagination for specific page numbers
1141
props.changePage((page - 1) * props.resultsPerPage)
1242
}
1343

@@ -25,7 +55,23 @@ const SearchResultsPaging = (props) => {
2555
const pageButton = (key, label, page, active) => {
2656
const classes = ["page-item"]
2757
if (active) classes.push("active")
28-
if (page < 1 || page > lastPage) classes.push("disabled")
58+
59+
// Check if button should be disabled
60+
let isDisabled = page < 1 || page > lastPage
61+
62+
// When using links-based pagination, also check link availability
63+
if (props.links && !isDisabled) {
64+
if (key === "next" && !props.links.next) {
65+
isDisabled = true
66+
} else if (key === "previous" && !props.links.prev) {
67+
isDisabled = true
68+
} else if (key === "first" && !props.links.first) {
69+
isDisabled = true
70+
}
71+
}
72+
73+
if (isDisabled) classes.push("disabled")
74+
2975
return (
3076
<li key={key} className={classes.join(" ")}>
3177
<button
@@ -88,6 +134,7 @@ SearchResultsPaging.propTypes = {
88134
totalResults: PropTypes.number.isRequired,
89135
resultsPerPage: PropTypes.number.isRequired,
90136
startOfRange: PropTypes.number.isRequired,
137+
links: PropTypes.object,
91138
}
92139

93140
export default SearchResultsPaging

src/reducers/search.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const setSearchResults = (state, action) => ({
2626
typeFilter: action.payload.options?.typeFilter,
2727
groupFilter: action.payload.options?.groupFilter,
2828
},
29+
links: action.payload.links,
2930
error: action.payload.error,
3031
},
3132
})

src/selectors/search.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ export const selectSearchOptions = (state, searchType) =>
2424
export const selectSearchResults = (state, searchType) =>
2525
state.search[searchType]?.results
2626

27+
export const selectSearchLinks = (state, searchType) =>
28+
state.search[searchType]?.links
29+
2730
export const selectHeaderSearch = (state) => state.editor.currentHeaderSearch

src/sinopiaSearch.js

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,38 @@ export const getSearchResultsWithFacets = async (
3636
if (Config.useResourceTemplateFixtures && hasFixtureResource(query))
3737
return Promise.resolve(resourceSearchResults(query))
3838

39-
const body = new URLSearchParams({ q: query })
39+
// Check if query is a full URL (for pagination links)
40+
let url
41+
const extractedOptions = { ...options }
42+
43+
if (
44+
query &&
45+
typeof query === "string" &&
46+
(query.startsWith("http://") || query.startsWith("https://"))
47+
) {
48+
url = query
49+
// Extract offset from URL query parameters
50+
try {
51+
const urlObj = new URL(query)
52+
const offset = urlObj.searchParams.get("offset")
53+
const limit = urlObj.searchParams.get("limit")
54+
if (offset !== null) {
55+
extractedOptions.startOfRange = parseInt(offset, 10)
56+
}
57+
if (limit !== null) {
58+
extractedOptions.resultsPerPage = parseInt(limit, 10)
59+
}
60+
} catch (e) {
61+
// If URL parsing fails, continue with original options
62+
}
63+
} else if (query) {
64+
const body = new URLSearchParams({ q: query })
65+
url = `${Config.searchHost}${Config.searchPath}?${body}`
66+
} else {
67+
// If query is null/undefined, construct URL without query parameter
68+
url = `${Config.searchHost}${Config.searchPath}`
69+
}
70+
4071
// const termsFilters = []
4172
// if (options.typeFilter) {
4273
// termsFilters.push({
@@ -73,7 +104,7 @@ export const getSearchResultsWithFacets = async (
73104
// },
74105
// }
75106
// }
76-
return fetchSearchResults(body, keycloak)
107+
return fetchSearchResultsFromUrl(url, keycloak, extractedOptions)
77108
}
78109

79110
export const getSearchResultsByUris = (resourceUris) => {
@@ -84,6 +115,8 @@ export const getSearchResultsByUris = (resourceUris) => {
84115
)
85116
return Promise.resolve(resourceSearchResults(resourceUris[0])[0])
86117

118+
// This function appears to be for a different use case
119+
// For now, keeping it as-is since it uses a different body format
87120
const body = {
88121
query: {
89122
terms: {
@@ -92,45 +125,47 @@ export const getSearchResultsByUris = (resourceUris) => {
92125
},
93126
size: resourceUris.length,
94127
}
95-
return fetchSearchResults(body).then((results) => results[0])
128+
// TODO: This needs to be updated to work with the new API
129+
const url = `${Config.searchHost}${Config.searchPath}`
130+
return fetchSearchResultsFromUrl(url, null).then((results) => results[0])
96131
}
97132

98-
const fetchSearchResults = (body, keycloak) => {
99-
const url = `${Config.searchHost}${Config.searchPath}?${body}`
100-
return fetch(url, {
133+
const fetchSearchResultsFromUrl = (url, keycloak, extractedOptions) => fetch(url, {
101134
method: "GET",
102135
})
103-
.then((resp) => {
104-
return resp.json()
105-
})
136+
.then((resp) => resp.json())
106137
.then((json) => {
107138
if (json.error) {
108139
return [
109140
{
110141
totalHits: 0,
111142
results: [],
112143
error: json.error.reason || json.error,
144+
options: extractedOptions,
113145
},
114146
undefined,
115147
]
116148
}
117-
return [hitsToResult(json), aggregationsToResult(json)]
149+
const result = hitsToResult(json)
150+
// Add extracted options to result
151+
result.options = extractedOptions
152+
return [result, aggregationsToResult(json)]
118153
})
119154
.catch((err) => [
120155
{
121156
totalHits: 0,
122157
results: [],
123158
error: err.toString(),
159+
options: extractedOptions,
124160
},
125161
undefined,
126162
])
127-
}
128163

129164
const hitsToResult = (payload) => {
130165
const results = []
131166
payload.results.forEach((hit) => {
132167
const types = hit.data["@type"]
133-
let rdfTypes = []
168+
const rdfTypes = []
134169
if (Array.isArray(types)) {
135170
types.forEach((type) =>
136171
rdfTypes.push(`http://id.loc.gov/ontologies/bibframe/${type}`)
@@ -145,21 +180,25 @@ const hitsToResult = (payload) => {
145180
// Handle array of titles
146181
if (Array.isArray(mainTitle)) {
147182
// Map each title to extract @value if it's an object, otherwise use as-is
148-
const titles = mainTitle.map(title => {
149-
if (title && typeof title === 'object' && '@value' in title) {
150-
return title['@value']
183+
const titles = mainTitle.map((title) => {
184+
if (title && typeof title === "object" && "@value" in title) {
185+
return title["@value"]
151186
}
152187
return title
153188
})
154189
// Join multiple titles with a separator
155-
label = titles.join(' / ')
156-
} else if (mainTitle && typeof mainTitle === 'object' && '@value' in mainTitle) {
190+
label = titles.join(" / ")
191+
} else if (
192+
mainTitle &&
193+
typeof mainTitle === "object" &&
194+
"@value" in mainTitle
195+
) {
157196
// Handle single JSON-LD object
158-
label = mainTitle['@value']
197+
label = mainTitle["@value"]
159198
}
160199
results.push({
161200
uri: hit.uri,
162-
label: label,
201+
label,
163202
created: hit.created_at,
164203
modified: hit.updated_at,
165204
type: rdfTypes,
@@ -168,8 +207,9 @@ const hitsToResult = (payload) => {
168207
})
169208
})
170209
return {
171-
totalHits: results.length,
172-
results: results,
210+
totalHits: payload.total,
211+
results,
212+
links: payload.links,
173213
}
174214
}
175215

@@ -308,8 +348,8 @@ const templateModFromBlueCore = (hit) => {
308348
id: resourceId,
309349
originalURI: hit.uri,
310350
remark: resourceRemark,
311-
resourceLabel: resourceLabel,
312-
resourceURI: resourceURI,
351+
resourceLabel,
352+
resourceURI,
313353
uri: bcURI,
314354
}
315355
}

0 commit comments

Comments
 (0)