Skip to content

Commit

Permalink
start useSearchApi
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherChudzicki committed Feb 23, 2024
1 parent b603ee2 commit d3c024a
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 16 deletions.
26 changes: 19 additions & 7 deletions src/revamp/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
LearningResourcesSearchRetrieveSortbyEnum as SortByEnum,
// ContentFile Search Enums
ContentFileSearchRetrieveSortbyEnum as ContentFileSortEnum,
ContentFileSearchRetrievePlatformEnum as ContentfilePlatformEnum,
} from "../open_api_generated";

type Endpoint = "resources" | "content_files";
Expand All @@ -16,12 +15,13 @@ type SearchParams = {
endpoint: Endpoint;
activeFacets: {
resource_type?: ResourceTypeEnum[];
deparment?: DepartmentEnum[];
department?: DepartmentEnum[];
course_feature?: string[];
content_feature_type?: string[];
level?: LevelEnum[];
platform?: PlatformEnum[] | ContentfilePlatformEnum[];
platform?: PlatformEnum[];
offered_by?: OfferedByEnum[];
topics?: string[];
topic?: string[];
certification?: boolean;
professional?: boolean;
};
Expand Down Expand Up @@ -61,7 +61,7 @@ const searchParamConfig: Record<Endpoint, EndpointParamConfig> = {
alias: "r",
isValid: withinEnum(Object.values(ResourceTypeEnum)),
},
deparment: {
department: {
alias: "d",
isValid: withinEnum(Object.values(DepartmentEnum)),
},
Expand All @@ -77,7 +77,7 @@ const searchParamConfig: Record<Endpoint, EndpointParamConfig> = {
alias: "o",
isValid: withinEnum(Object.values(OfferedByEnum)),
},
topics: {
topic: {
alias: "t",
isValid: (value: string) => value.length > 0,
},
Expand All @@ -91,6 +91,10 @@ const searchParamConfig: Record<Endpoint, EndpointParamConfig> = {
isValid: (value: string) => value === "true",
isBoolean: true,
},
course_feature: {
alias: "cf",
isValid: (value: string) => value.length > 0,
},
},
},
content_files: {
Expand All @@ -99,14 +103,22 @@ const searchParamConfig: Record<Endpoint, EndpointParamConfig> = {
isValid: withinEnum(Object.values(ContentFileSortEnum)),
},
facets: {
offered_by: {
alias: "o",
isValid: withinEnum(Object.values(OfferedByEnum)),
},
platform: {
alias: "p",
isValid: withinEnum(Object.values(ContentfilePlatformEnum)),
isValid: withinEnum(Object.values(PlatformEnum)),
},
content_feature_type: {
alias: "cf",
isValid: (value: string) => value.length > 0,
},
topic: {
alias: "t",
isValid: (value: string) => value.length > 0,
},
},
},
};
Expand Down
117 changes: 117 additions & 0 deletions src/revamp/useInfiniteSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { SearchParams } from "./configs";
import { getSearchUrl } from "./util";
import type {
SearchResponse,
ContentFileSearchRetrieveAggregationsEnum as ContentFileAggregationsEnum,
LearningResourcesSearchRetrieveAggregationsEnum as ResourceAggregationsEnum,
} from "../open_api_generated";

type Status = "pending" | "error" | "success";

type UseInfiniteSearchApiResult = {
pages: SearchResponse[];
error?: unknown;
status: Status;
isFetchingNextPage: boolean;
hasNextPage: boolean;
fetchNextPage: () => Promise<void>;
};

type AggregationsConfig = {
resources: ResourceAggregationsEnum[];
content_files: ContentFileAggregationsEnum[];
};

type UseInfiniteSearchApiProps = {
params: SearchParams;
baseUrl: string;
limit?: number;
aggregations?: AggregationsConfig;
makeRequest?: (url: string) => Promise<{ data: any }>;
};

const DEFAULT_LIMIT = 10;

const defaultMakeRequest = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const data = await response.json();
return { data };
};

const useInfiniteSearchApi = ({
params,
limit = DEFAULT_LIMIT,
makeRequest = defaultMakeRequest,
baseUrl,
aggregations,
}: UseInfiniteSearchApiProps): UseInfiniteSearchApiResult => {
const [nextPage, setNextPage] = useState(0);
const [error, setError] = useState<unknown>();
const [pages, setPages] = useState<SearchResponse[]>([]);
const [status, setStatus] = useState<Status>("pending");
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
const urlRef = useRef<string | null>();

const hasNextPage =
pages[0] === undefined || pages[0]?.count > nextPage * limit;

const getPageUrl = useCallback(
(page: number) => {
const offset = page * limit;
return getSearchUrl(baseUrl, {
limit,
offset,
...params,
aggregations: aggregations?.[params.endpoint] ?? [],
});
},
[aggregations, baseUrl, limit, params]
);

const fetchNextPage = useCallback(async () => {
if (!hasNextPage || isFetchingNextPage) return;
const url = getPageUrl(nextPage);
urlRef.current = url;
try {
setIsFetchingNextPage(true);
const { data } = await makeRequest(url);
if (url !== urlRef.current) return;
urlRef.current = null;
setIsFetchingNextPage(false);
setStatus("success");
setPages((pages) => {
return [...pages, data];
});
setNextPage(nextPage + 1);
} catch (err) {
if (url !== urlRef.current) return;
setStatus("error");
setError(err);
}
}, [getPageUrl, hasNextPage, isFetchingNextPage, makeRequest, nextPage]);

useEffect(() => {
setNextPage(0);
setPages([]);
setError(undefined);
setIsFetchingNextPage(false);
setStatus("pending");
urlRef.current = null;
fetchNextPage();
}, [getPageUrl(0)]);

Check warning on line 105 in src/revamp/useInfiniteSearch.ts

View workflow job for this annotation

GitHub Actions / javascript-tests

React Hook useEffect has a missing dependency: 'fetchNextPage'. Either include it or remove the dependency array

Check warning on line 105 in src/revamp/useInfiniteSearch.ts

View workflow job for this annotation

GitHub Actions / javascript-tests

React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked

return {
pages,
error,
status,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
};
};

export default useInfiniteSearchApi;
18 changes: 9 additions & 9 deletions src/revamp/useSearchQueryParams.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test("Extracts expected facets from the given URLSearchParams for endpoint=resou
queryText: "",
activeFacets: {
resource_type: ["course"],
deparment: ["6", "8"],
department: ["6", "8"],
},
endpoint: "resources",
});
Expand Down Expand Up @@ -95,7 +95,7 @@ test("Extracts expected facets from the given URLSearchParams for endpoint=resou
assertParamsExtracted("?l=graduate&t=python&t=javascript", {
queryText: "",
activeFacets: {
topics: ["python", "javascript"],
topic: ["python", "javascript"],
level: ["graduate"],
},
endpoint: "resources",
Expand Down Expand Up @@ -151,11 +151,11 @@ test("Ignores invalid facet values", () => {
queryText: "",
activeFacets: {
resource_type: ["course"],
deparment: ["6"],
department: ["6"],
level: ["noncredit"],
platform: ["ocw"],
offered_by: ["ocw"],
topics: ["python", "all-topics-allowed"],
topic: ["python", "all-topics-allowed"],
certification: true,
professional: true,
},
Expand All @@ -166,12 +166,12 @@ test("Ignores invalid facet values", () => {
test.each([
{
initial: "?r=course&d=6&cf=whatever",
expectedFacets: { resource_type: ["course"], deparment: ["6"] },
expectedFacets: { resource_type: ["course"], department: ["6"] },
expectedEndpoint: "resources",
},
{
initial: "?r=course&d=6&cf=whatever&e=resources",
expectedFacets: { resource_type: ["course"], deparment: ["6"] },
expectedFacets: { resource_type: ["course"], department: ["6"] },
expectedEndpoint: "resources",
},
{
Expand All @@ -193,7 +193,7 @@ test.each([
test("Ignores / keeps sort values based on endpoint", () => {
assertParamsExtracted("?d=6&s=mitcoursenumber", {
endpoint: "resources",
activeFacets: { deparment: ["6"] },
activeFacets: { department: ["6"] },
sort: "mitcoursenumber",
queryText: "",
});
Expand Down Expand Up @@ -250,7 +250,7 @@ test("Setting current text does not affect query parameters at all", () => {
queryText: "python",
activeFacets: {
resource_type: ["course"],
deparment: ["6"],
department: ["6"],
},
endpoint: "resources",
});
Expand All @@ -268,7 +268,7 @@ test("Setting current text does not affect query parameters at all", () => {
queryText: "python",
activeFacets: {
resource_type: ["course"],
deparment: ["6"],
department: ["6"],
},
endpoint: "resources",
});
Expand Down
56 changes: 56 additions & 0 deletions src/revamp/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { SearchParams, Endpoint } from "./configs";

const endpointUrls: Record<Endpoint, string> = {
resources: "api/v1/learning_resources_search/",
content_files: "api/v1/content_file_search/",
};

export const getSearchUrl = (
baseUrl: string,
{
endpoint,
queryText,
sort,
activeFacets,
aggregations,
limit,
offset,
}: SearchParams & {
aggregations: string[];
limit?: number;
offset?: number;
}
): string => {
const url = new URL(endpointUrls[endpoint], baseUrl);

if (queryText) {
url.searchParams.append("q", queryText);
}
if (offset) {
url.searchParams.append("offset", offset.toString());
}

if (limit) {
url.searchParams.append("limit", limit.toString());
}

if (sort) {
url.searchParams.append("sortby", sort);
}

if (aggregations && aggregations.length > 0) {
url.searchParams.append("aggregations", aggregations.join(","));
}

if (activeFacets) {
for (const [key, value] of Object.entries(activeFacets)) {
const asArray = Array.isArray(value) ? value : [value];
if (asArray.length > 0) {
url.searchParams.append(key, asArray.join(","));
}
}
}

url.searchParams.sort();
return url.toString();
};

0 comments on commit d3c024a

Please sign in to comment.