Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UILD-399: Browse function for Authority lookup #14

Merged
merged 23 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c8323a
default selected Search controls values
SKarolFolio Oct 15, 2024
d1eb365
load search results when Complex lookup has the selected value
SKarolFolio Oct 16, 2024
a014a72
MODLD-547: prepare for folio-org space (#1)
PBobylev Oct 16, 2024
c57ea56
MODLD-547: change package to conventional 'folio' (#9)
PBobylev Oct 17, 2024
92262f8
MODLD-547: fix for broken tests (#8)
SKarolFolio Oct 18, 2024
58e6ed5
add Browse results processing
SKarolFolio Oct 18, 2024
8bc7806
table styles fix
SKarolFolio Oct 18, 2024
1cdb03a
Browse result processing
SKarolFolio Oct 18, 2024
1828243
load search results
SKarolFolio Oct 21, 2024
29cdc45
display search results count
SKarolFolio Oct 21, 2024
cd21f40
update MARC preview styles
SKarolFolio Oct 22, 2024
b8e3d8a
query builder refactoring
SKarolFolio Oct 23, 2024
f9eb350
update search query
SKarolFolio Oct 23, 2024
fc76f8f
pagination component - add new property and extend config
SKarolFolio Oct 23, 2024
8a4af3b
add Search and Browse pagination
SKarolFolio Oct 24, 2024
ec35c70
Merge branch 'master' of https://github.com/folio-org/ui-linked-data …
SKarolFolio Oct 24, 2024
57aa920
pagination fix
SKarolFolio Oct 24, 2024
5ef9acf
code refactoring
SKarolFolio Oct 24, 2024
a67200d
minor refactoring and unit test fix
SKarolFolio Oct 24, 2024
fdb1b3d
code refactoring: fetch search data
SKarolFolio Oct 24, 2024
72e9e3f
add tests for formatter
SKarolFolio Oct 25, 2024
1b94e00
additional unit tests
SKarolFolio Oct 25, 2024
f4f9dea
refactoring after code review
SKarolFolio Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/common/api/search.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,35 @@ export const getSearchData = (url?: string, urlParams?: Record<string, string>)
};

export const getSearchResults = async (params: Record<string, string | number>) => {
const { endpointUrl, query, offset = '0', limit = SEARCH_RESULTS_LIMIT.toString(), resultsContainer } = params;
const {
endpointUrl,
query,
offset,
limit = SEARCH_RESULTS_LIMIT.toString(),
resultsContainer,
precedingRecordsCount,
} = params;

const urlParams: Record<string, string> | undefined = {
query: query as string,
offset: offset?.toString(),
limit: limit?.toString(),
};

if (offset) {
urlParams.offset = offset?.toString();
}

if (precedingRecordsCount) {
urlParams.precedingRecordsCount = precedingRecordsCount.toString();
}

const result = await baseApi.getJson({ url: endpointUrl as string, urlParams });

return {
content: result.content ?? result[resultsContainer],
totalRecords: result.totalRecords,
totalPages: result.totalPages ?? Math.ceil(result?.totalRecords / +limit),
prev: result.prev,
next: result.next,
};
};
6 changes: 6 additions & 0 deletions src/common/constants/complexLookup.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export enum SearchableIndex {
Genre = 'genre',
}

export enum SearchableIndexQuerySelector {
Query = 'query',
Prev = 'prev',
Next = 'next',
}

export const COMPLEX_LOOKUPS_LINKED_FIELDS_MAPPING = {
subclass: {
PERSON: {
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants/search.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export type AdvancedSearchSchema = AdvancedSearchSchemaRow[];

export const SEARCH_RESULTS_LIMIT = 10;

export const BROWSE_PRECEDING_RECORDS_COUNT = 5;

export const SELECT_IDENTIFIERS = Object.values(SearchIdentifiers);

export const SELECT_OPERATORS = Object.values(AdvancedSearchOperators);
Expand Down
8 changes: 6 additions & 2 deletions src/common/helpers/search/formatters/authorities.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { v4 as uuidv4 } from 'uuid';

export const formatAuthorityItem = (
authoritiesList: AuthorityAsSearchResultDTO[],
authoritiesList: AuthorityAsSearchResultDTO[] | AuthorityAsBrowseResultDTO[],
sourceData?: SourceDataDTO,
): SearchResultsTableRow[] =>
authoritiesList?.map(({ id, authRefType, headingRef, headingType, sourceFileId }) => {
authoritiesList?.map(authorityEntry => {
const selectedEntry = (authorityEntry.authority ?? authorityEntry) as AuthorityAsSearchResultDTO;
const { id = '', authRefType = '', headingRef = '', headingType = '', sourceFileId = '' } = selectedEntry;
const sourceLabel = sourceData?.find(({ id: sourceId }) => sourceId === sourceFileId)?.name ?? sourceFileId;
const { isAnchor } = authorityEntry;

return {
__meta: {
id,
key: uuidv4(),
isAnchor,
},
authorized: {
label: authRefType,
Expand Down
1 change: 1 addition & 0 deletions src/common/helpers/search/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const SEARCH_RESULTS_FORMATTER: Record<
string,
(data: any, sourceData?: SourceDataDTO) => SearchResultsTableRow[]
> = {
default: formatAuthorityItem,
authorities: formatAuthorityItem,
};
15 changes: 15 additions & 0 deletions src/common/helpers/search/queryBuilder/authorities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SearchableIndexQuerySelector } from '@common/constants/complexLookup.constants';
import { SEARCH_QUERY_VALUE_PARAM } from '@common/constants/search.constants';

export const buildSearchQuery = ({
map,
selector = SearchableIndexQuerySelector.Query,
searchBy,
value,
}: BuildSearchQueryParams) => {
const searchableIndex = map?.[searchBy];

return searchableIndex?.[selector]?.replaceAll(SEARCH_QUERY_VALUE_PARAM, value);
};

export const buildBrowseQuery = () => {};
10 changes: 9 additions & 1 deletion src/common/helpers/search/queryBuilder/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export { buildSearchQuery } from './queryBuilder';
import { buildSearchQuery } from './authorities';

export const SEARCH_QUERY_BUILDER: Record<
string,
({ map, selector, searchBy, value }: BuildSearchQueryParams) => string | undefined
> = {
default: buildSearchQuery,
authorities: buildSearchQuery,
};
7 changes: 0 additions & 7 deletions src/common/helpers/search/queryBuilder/queryBuilder.ts

This file was deleted.

16 changes: 1 addition & 15 deletions src/common/hooks/useComplexLookupApi.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { useEffect } from 'react';
import { useResetRecoilState } from 'recoil';
import state from '@state';
import { useSearchFiltersData } from './useSearchFiltersData';

export const useComplexLookupApi = (api: ComplexLookupApiEntryConfig, filters: SearchFilters, isOpen: boolean) => {
const resetFacetsData = useResetRecoilState(state.search.facetsData);
export const useComplexLookupApi = (api: ComplexLookupApiEntryConfig, filters: SearchFilters) => {
const { getSearchSourceData, getSearchFacetsData } = useSearchFiltersData();

const getFacetsData = async (facet?: string, isOpen?: boolean) => {
Expand All @@ -18,16 +14,6 @@ export const useComplexLookupApi = (api: ComplexLookupApiEntryConfig, filters: S
await getFacetsData(openedFilter?.facet);
};

useEffect(() => {
if (!isOpen || !api.endpoints.source) return;

getSourceData();

return () => {
resetFacetsData();
};
}, [isOpen]);

return {
getFacetsData,
getSourceData,
Expand Down
201 changes: 201 additions & 0 deletions src/common/hooks/useFetchSearchData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { useCallback } from 'react';
import { useSetRecoilState, useRecoilState, SetterOrUpdater, useResetRecoilState } from 'recoil';
import { getByIdentifier } from '@common/api/search.api';
import { SearchIdentifiers, SearchSegment } from '@common/constants/search.constants';
import { SearchableIndexQuerySelector } from '@common/constants/complexLookup.constants';
import { StatusType } from '@common/constants/status.constants';
import { normalizeQuery } from '@common/helpers/search.helper';
import { normalizeLccn } from '@common/helpers/validations.helper';
import { UserNotificationFactory } from '@common/services/userNotification';
import state from '@state';
import { useSearchContext } from './useSearchContext';

export const useFetchSearchData = () => {
const {
endpointUrl,
searchFilter,
isSortedResults,
navigationSegment,
endpointUrlsBySegments,
searchResultsLimit,
fetchSearchResults,
searchResultsContainer,
searchableIndicesMap,
buildSearchQuery,
precedingRecordsCount,
} = useSearchContext();
const setIsLoading = useSetRecoilState(state.loadingState.isLoading);
const setMessage = useSetRecoilState(state.search.message);
const [data, setData] = useRecoilState(state.search.data);
const resetData = useResetRecoilState(state.search.data);
const setPageMetadata = useSetRecoilState(state.search.pageMetadata);
const setStatusMessages = useSetRecoilState(state.status.commonMessages);
const resetStatusMessage = useResetRecoilState(state.status.commonMessages);

const validateAndNormalizeQuery = useCallback(
(type: SearchIdentifiers, query: string) => {
if (type === SearchIdentifiers.LCCN) {
const normalized = normalizeLccn(query);

!normalized && setMessage('ld.searchInvalidLccn');

return normalized;
}

return normalizeQuery(query);
},
[setMessage],
);

const getEndpointUrl = ({
selectedSegment,
endpointUrlsBySegments,
endpointUrl,
}: {
selectedSegment?: string;
endpointUrlsBySegments?: EndpointUrlsBySegments;
endpointUrl: string;
}) => {
return selectedSegment ? endpointUrlsBySegments?.[selectedSegment] : endpointUrl;
};

const generateQuery = ({
searchBy,
query,
selectedSegment,
searchableIndicesMap,
baseQuerySelector,
}: {
searchBy: SearchableIndexType;
query: string;
selectedSegment?: string;
searchableIndicesMap?: SearchableIndicesMap;
baseQuerySelector?: SearchableIndexQuerySelector;
}) => {
const selectedSearchableIndices = selectedSegment
? searchableIndicesMap?.[selectedSegment as SearchSegmentValue]
: searchableIndicesMap;

return (
buildSearchQuery?.({
map: selectedSearchableIndices as SearchableIndexEntries,
selector: baseQuerySelector,
searchBy,
value: query,
}) ?? query
);
};

const fetchDataFromApi = async ({
endpointUrl,
searchFilter,
isSortedResults,
searchBy,
query,
offset,
limit,
precedingRecordsCount,
resultsContainer,
}: {
endpointUrl: string;
searchFilter?: string;
isSortedResults?: boolean;
searchBy: SearchIdentifiers;
query: string;
offset?: string;
limit?: string;
precedingRecordsCount?: number;
resultsContainer: any;
}) => {
return fetchSearchResults
? await fetchSearchResults({
endpointUrl,
searchFilter,
isSortedResults,
searchBy,
query,
offset,
limit,
precedingRecordsCount,
resultsContainer,
})
: await getByIdentifier({
endpointUrl,
searchFilter,
isSortedResults,
searchBy,
query,
offset,
limit,
});
};

const handleFetchError = (setStatusMessages: SetterOrUpdater<StatusEntry[]>) => {
setStatusMessages(currentStatus => [
...currentStatus,
UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching'),
]);
};

const fetchData = useCallback(
async ({
query,
searchBy,
offset,
selectedSegment,
baseQuerySelector = SearchableIndexQuerySelector.Query,
}: FetchDataParams) => {
resetStatusMessage();
const selectedNavigationSegment = selectedSegment ?? navigationSegment?.value;

data && resetData();

const updatedQuery = validateAndNormalizeQuery(searchBy, query);
if (!updatedQuery) return;

setIsLoading(true);

try {
const currentEndpointUrl = getEndpointUrl({
selectedSegment: selectedNavigationSegment,
endpointUrlsBySegments,
endpointUrl,
});
const generatedQuery = generateQuery({
searchBy,
query: updatedQuery,
selectedSegment: selectedNavigationSegment,
searchableIndicesMap,
baseQuerySelector,
});
const isBrowseSearch = selectedNavigationSegment === SearchSegment.Browse;

const result = await fetchDataFromApi({
endpointUrl: currentEndpointUrl ?? '',
searchFilter,
isSortedResults,
searchBy,
query: generatedQuery,
offset: offset?.toString(),
limit: searchResultsLimit?.toString(),
precedingRecordsCount: isBrowseSearch ? precedingRecordsCount : undefined,
resultsContainer: searchResultsContainer?.[selectedNavigationSegment as SearchSegmentValue],
});

const { content, totalPages, totalRecords, prev, next } = result;

if (!content.length) return setMessage('ld.searchNoRdsMatch');

setData(content);
setPageMetadata({ totalPages, totalElements: totalRecords, prev, next });
} catch {
handleFetchError(setStatusMessages);
} finally {
setIsLoading(false);
}
},
[data, endpointUrl, fetchSearchResults, navigationSegment?.value, searchFilter, isSortedResults],
);

return { fetchData };
};
Loading
Loading