From 8c451dedb39c2cb8a9f3c6194dd7c7a837a1e7e8 Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Tue, 9 Apr 2024 16:13:43 -0400 Subject: [PATCH] Channel Search (#740) --- frontends/api/src/clients.ts | 4 + .../api/src/hooks/learningResources/index.ts | 12 + .../src/hooks/learningResources/keyFactory.ts | 8 + .../test-utils/factories/learningResources.ts | 2 + frontends/api/src/test-utils/urls.ts | 7 + .../EditFieldAppearanceForm.test.tsx | 2 + .../src/pages/FieldPage/FieldPage.test.tsx | 63 +++++- .../src/pages/FieldPage/FieldPage.tsx | 14 ++ .../src/pages/FieldPage/FieldSearch.test.tsx | 202 +++++++++++++++++ .../src/pages/FieldPage/FieldSearch.tsx | 211 ++++++++++++++++++ .../FieldPage/FieldSearchFacetDisplay.tsx | 130 +++++++++++ .../src/pages/SearchPage/SearchPage.tsx | 3 +- frontends/ol-components/src/index.ts | 5 + 13 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 frontends/mit-open/src/pages/FieldPage/FieldSearch.test.tsx create mode 100644 frontends/mit-open/src/pages/FieldPage/FieldSearch.tsx create mode 100644 frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx diff --git a/frontends/api/src/clients.ts b/frontends/api/src/clients.ts index 0ec8c47c62..8fb84dbbfd 100644 --- a/frontends/api/src/clients.ts +++ b/frontends/api/src/clients.ts @@ -7,6 +7,7 @@ import { ArticlesApi, ProgramLettersApi, LearningResourcesSearchApi, + PlatformsApi, } from "./generated/v1/api" import { ChannelsApi, WidgetListsApi } from "./generated/v0/api" @@ -36,6 +37,8 @@ const userListsApi = new UserlistsApi(undefined, BASE_PATH, axiosInstance) const offerorsApi = new OfferorsApi(undefined, BASE_PATH, axiosInstance) +const platformsApi = new PlatformsApi(undefined, BASE_PATH, axiosInstance) + const topicsApi = new TopicsApi(undefined, BASE_PATH, axiosInstance) const articlesApi = new ArticlesApi(undefined, BASE_PATH, axiosInstance) @@ -60,4 +63,5 @@ export { learningResourcesSearchApi, channelsApi, widgetListsApi, + platformsApi, } diff --git a/frontends/api/src/hooks/learningResources/index.ts b/frontends/api/src/hooks/learningResources/index.ts index c5009fff18..41a9a75dd9 100644 --- a/frontends/api/src/hooks/learningResources/index.ts +++ b/frontends/api/src/hooks/learningResources/index.ts @@ -26,6 +26,7 @@ import type { UserList, UserListRelationshipRequest, MicroUserListRelationship, + PlatformsApiPlatformsListRequest, } from "../../generated/v1" import learningResources, { invalidateResourceQueries, @@ -413,6 +414,16 @@ const useListItemMove = () => { }) } +const usePlatformsList = ( + params: PlatformsApiPlatformsListRequest = {}, + opts: Pick = {}, +) => { + return useQuery({ + ...learningResources.platforms(params), + ...opts, + }) +} + export { useLearningResourcesList, useLearningResourcesUpcoming, @@ -439,4 +450,5 @@ export { useInfiniteUserListItems, useOfferorsList, useListItemMove, + usePlatformsList, } diff --git a/frontends/api/src/hooks/learningResources/keyFactory.ts b/frontends/api/src/hooks/learningResources/keyFactory.ts index a3549a6633..344d3d06d8 100644 --- a/frontends/api/src/hooks/learningResources/keyFactory.ts +++ b/frontends/api/src/hooks/learningResources/keyFactory.ts @@ -6,6 +6,7 @@ import { topicsApi, userListsApi, offerorsApi, + platformsApi, } from "../../clients" import axiosInstance from "../../axios" import type { @@ -23,6 +24,7 @@ import type { PaginatedUserListRelationshipList, UserList, OfferorsApiOfferorsListRequest, + PlatformsApiPlatformsListRequest, } from "../../generated/v1" import { createQueryKeys } from "@lukemorales/query-key-factory" @@ -129,6 +131,12 @@ const learningResources = createQueryKeys("learningResources", { queryFn: () => offerorsApi.offerorsList(params).then((res) => res.data), } }, + platforms: (params: PlatformsApiPlatformsListRequest) => { + return { + queryKey: [params], + queryFn: () => platformsApi.platformsList(params).then((res) => res.data), + } + }, }) const learningPathHasResource = diff --git a/frontends/api/src/test-utils/factories/learningResources.ts b/frontends/api/src/test-utils/factories/learningResources.ts index c0e5901a83..a18ca8731c 100644 --- a/frontends/api/src/test-utils/factories/learningResources.ts +++ b/frontends/api/src/test-utils/factories/learningResources.ts @@ -93,6 +93,7 @@ const learningResourceOfferor: Factory = ( } } const learningResourceOfferors = makePaginatedFactory(learningResourceOfferor) +const learningResourcePlatforms = makePaginatedFactory(learningResourcePlatform) const learningResourceRun: Factory = (overrides = {}) => { const start = overrides.start_date @@ -391,6 +392,7 @@ export { learningResourceDepartment as department, learningResourceTopics as topics, learningResourceOfferors as offerors, + learningResourcePlatforms as platforms, learningPath, learningPaths, microLearningPathRelationship, diff --git a/frontends/api/src/test-utils/urls.ts b/frontends/api/src/test-utils/urls.ts index 8439a5abdd..1b2e05de58 100644 --- a/frontends/api/src/test-utils/urls.ts +++ b/frontends/api/src/test-utils/urls.ts @@ -12,6 +12,7 @@ import type { ArticlesApi, UserlistsApi, OfferorsApi, + PlatformsApi, } from "../generated/v1" import type { BaseAPI } from "../generated/v1/base" @@ -44,6 +45,11 @@ const offerors = { `/api/v1/offerors/${query(params)}`, } +const platforms = { + list: (params?: Params) => + `/api/v1/platforms/${query(params)}`, +} + const topics = { list: (params?: Params) => `/api/v1/topics/${query(params)}`, @@ -118,4 +124,5 @@ export { fields, widgetLists, offerors, + platforms, } diff --git a/frontends/mit-open/src/pages/FieldPage/EditFieldAppearanceForm.test.tsx b/frontends/mit-open/src/pages/FieldPage/EditFieldAppearanceForm.test.tsx index 78cc790821..d6e644c1e0 100644 --- a/frontends/mit-open/src/pages/FieldPage/EditFieldAppearanceForm.test.tsx +++ b/frontends/mit-open/src/pages/FieldPage/EditFieldAppearanceForm.test.tsx @@ -13,6 +13,8 @@ import type { FieldChannel } from "api/v0" const setupApis = (fieldOverrides: Partial) => { const field = factory.field({ is_moderator: true, ...fieldOverrides }) + field.search_filter = undefined + setMockResponse.get( urls.fields.details(field.channel_type, field.name), field, diff --git a/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx b/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx index b90dc75e82..d5ee299a78 100644 --- a/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx +++ b/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx @@ -1,7 +1,8 @@ import { assertInstanceOf } from "ol-utilities" -import { urls } from "api/test-utils" +import { urls, factories } from "api/test-utils" import type { FieldChannel } from "api/v0" -import { fields as factory } from "api/test-utils/factories" +import type { LearningResourceSearchResponse } from "api" + import WidgetList from "./WidgetsList" import { renderTestApp, @@ -13,6 +14,7 @@ import { } from "../../test-utils" import { makeWidgetListResponse } from "ol-widgets/src/factories" import { makeFieldViewPath } from "@/common/urls" +import FieldSearch from "./FieldSearch" jest.mock("./WidgetsList", () => { const actual = jest.requireActual("./WidgetsList") @@ -23,8 +25,20 @@ jest.mock("./WidgetsList", () => { }) const mockWidgetList = jest.mocked(WidgetList) -const setupApis = (fieldPatch?: Partial) => { - const field = factory.field(fieldPatch) +jest.mock("./FieldSearch", () => { + const actual = jest.requireActual("./FieldSearch") + return { + __esModule: true, + default: jest.fn(actual.default), + } +}) +const mockedFieldSearch = jest.mocked(FieldSearch) + +const setupApis = ( + fieldPatch?: Partial, + search?: Partial, +) => { + const field = factories.fields.field(fieldPatch) setMockResponse.get( urls.fields.details(field.channel_type, field.name), @@ -37,6 +51,23 @@ const setupApis = (fieldPatch?: Partial) => { widgetsList, ) + setMockResponse.get( + urls.platforms.list(), + factories.learningResources.platforms({ count: 5 }), + ) + + setMockResponse.get(expect.stringContaining(urls.search.resources()), { + count: 0, + next: null, + previous: null, + results: [], + metadata: { + aggregations: {}, + suggestions: [], + }, + ...search, + }) + return { field, widgets: widgetsList.widgets, @@ -64,6 +95,30 @@ describe("FieldPage", () => { ]) }) + it("Displays the field search if search_filter is not undefined", async () => { + const { field } = setupApis({ search_filter: "platform=ocw" }) + renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) + await screen.findByText(field.title) + const expectedProps = expect.objectContaining({ + constantSearchParams: { platform: ["ocw"] }, + }) + const expectedContext = expect.anything() + + expect(mockedFieldSearch).toHaveBeenLastCalledWith( + expectedProps, + expectedContext, + ) + }) + + it("Does not display the field search if search_filter is undefined", async () => { + const { field } = setupApis() + field.search_filter = undefined + renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) + await screen.findByText(field.title) + + expect(mockedFieldSearch).toHaveBeenCalledTimes(0) + }) + it.each([ { getUrl: (field: FieldChannel) => `/c/${field.channel_type}/${field.name}`, diff --git a/frontends/mit-open/src/pages/FieldPage/FieldPage.tsx b/frontends/mit-open/src/pages/FieldPage/FieldPage.tsx index 631bb52ad4..5a49bc6e65 100644 --- a/frontends/mit-open/src/pages/FieldPage/FieldPage.tsx +++ b/frontends/mit-open/src/pages/FieldPage/FieldPage.tsx @@ -7,6 +7,8 @@ import { useChannelDetail } from "api/hooks/fields" import WidgetsList from "./WidgetsList" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" import { makeFieldViewPath } from "@/common/urls" +import FieldSearch from "./FieldSearch" +import type { Facets, FacetKey } from "@mitodl/course-search-utils" type RouteParams = { channelType: string @@ -42,6 +44,15 @@ const FieldPage: React.FC = () => { navigate(makeFieldViewPath(String(channelType), String(name))) }, [navigate, channelType, name]) + const searchParams: Facets = {} + + if (fieldQuery.data?.search_filter) { + const urlParams = new URLSearchParams(fieldQuery.data.search_filter) + for (const [key, value] of urlParams.entries()) { + searchParams[key as FacetKey] = value.split(",") + } + } + return ( @@ -68,6 +79,9 @@ const FieldPage: React.FC = () => {

{fieldQuery.data?.public_description}

+ {fieldQuery.data?.search_filter && ( + + )}
diff --git a/frontends/mit-open/src/pages/FieldPage/FieldSearch.test.tsx b/frontends/mit-open/src/pages/FieldPage/FieldSearch.test.tsx new file mode 100644 index 0000000000..70b532bf51 --- /dev/null +++ b/frontends/mit-open/src/pages/FieldPage/FieldSearch.test.tsx @@ -0,0 +1,202 @@ +import { screen, within, user, waitFor, renderTestApp } from "@/test-utils" +import { setMockResponse, urls, factories, makeRequest } from "api/test-utils" +import type { LearningResourceSearchResponse } from "api" +import invariant from "tiny-invariant" +import { makeWidgetListResponse } from "ol-widgets/src/factories" +import type { FieldChannel } from "api/v0" + +const setMockApiResponses = ({ + search, + fieldPatch, +}: { + search?: Partial + fieldPatch?: Partial +}) => { + const field = factories.fields.field(fieldPatch) + + setMockResponse.get( + urls.fields.details(field.channel_type, field.name), + field, + ) + + const widgetsList = makeWidgetListResponse() + setMockResponse.get( + urls.widgetLists.details(field.widget_list || -1), + widgetsList, + ) + + setMockResponse.get( + urls.platforms.list(), + factories.learningResources.platforms({ count: 5 }), + ) + + setMockResponse.get(expect.stringContaining(urls.search.resources()), { + count: 0, + next: null, + previous: null, + results: [], + metadata: { + aggregations: {}, + suggestions: [], + }, + ...search, + }) + + return { + field, + } +} + +const getLastApiSearchParams = () => { + const call = makeRequest.mock.calls.find(([method, url]) => { + if (method !== "get") return false + return url.startsWith(urls.search.resources()) + }) + invariant(call) + const [_method, url] = call + const fullUrl = new URL(url, "http://mit.edu") + return fullUrl.searchParams +} + +describe("FieldSearch", () => { + test("Renders search results", async () => { + const resources = factories.learningResources.resources({ + count: 10, + }).results + const { field } = setMockApiResponses({ + search: { + count: 1000, + metadata: { + aggregations: { + resource_type: [ + { key: "course", doc_count: 100 }, + { key: "podcast", doc_count: 200 }, + { key: "program", doc_count: 300 }, + { key: "irrelevant", doc_count: 400 }, + ], + }, + suggestions: [], + }, + results: resources, + }, + }) + renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) + await screen.findByText(field.title) + const tabpanel = await screen.findByRole("tabpanel") + const headings = await within(tabpanel).findAllByRole("heading") + expect(headings.map((h) => h.textContent)).toEqual( + resources.map((r) => r.title), + ) + }) + + test.each([ + { + searchFilter: "offered_by=ocw", + url: "?topic=physics", + expected: { offered_by: "ocw", topic: "physics" }, + }, + { + searchFilter: "resource_type=program,course", + url: "?resource_type=course", + expected: { resource_type: "course" }, + }, + { + searchFilter: "resource_type=program", + url: "?resource_type=course", + expected: { resource_type: "course" }, + }, + ])( + "Makes API call with correct facets and aggregations", + async ({ searchFilter, url, expected }) => { + const { field } = setMockApiResponses({ + fieldPatch: { search_filter: searchFilter }, + search: { + count: 700, + metadata: { + aggregations: { + topic: [ + { key: "physics", doc_count: 100 }, + { key: "chemistry", doc_count: 200 }, + ], + }, + suggestions: [], + }, + }, + }) + renderTestApp({ url: `/c/${field.channel_type}/${field.name}/${url}` }) + + await waitFor(() => { + expect(makeRequest.mock.calls.length > 0).toBe(true) + }) + const apiSearchParams = getLastApiSearchParams() + expect(apiSearchParams.getAll("aggregations").sort()).toEqual([ + "platform", + "resource_type", + "topic", + ]) + expect(Object.fromEntries(apiSearchParams.entries())).toEqual( + expect.objectContaining(expected), + ) + }, + ) + + test("Displaying and toggling facets", async () => { + const { field } = setMockApiResponses({ + fieldPatch: { search_filter: "topic=physics,chemistry" }, + search: { + count: 700, + metadata: { + aggregations: { + topic: [ + { key: "physics", doc_count: 100 }, + { key: "chemistry", doc_count: 200 }, + { key: "literature", doc_count: 200 }, + ], + resource_type: [ + { key: "course", doc_count: 100 }, + { key: "program", doc_count: 100 }, + ], + }, + suggestions: [], + }, + }, + }) + + const { location } = renderTestApp({ + url: `/c/${field.channel_type}/${field.name}/`, + }) + expect(location.current.search).toBe("") + + const resourceTypeDropdown = await screen.findByText("resource type") + + expect(screen.queryByText("topic")).toBeNull() + + await user.click(resourceTypeDropdown) + + let courseSelect = await screen.findByRole("option", { + name: /Course/i, + }) + + expect(courseSelect).toHaveAttribute("aria-selected", "false") + + await user.click(courseSelect) + + expect(location.current.search).toBe("?resource_type=course") + + courseSelect = await screen.findByRole("option", { + name: /Course/i, + }) + + expect(courseSelect).toHaveAttribute("aria-selected", "true") + + await user.click(courseSelect) + + expect(location.current.search).toBe("") + + courseSelect = await screen.findByRole("option", { + name: /Course/i, + }) + + expect(courseSelect).toHaveAttribute("aria-selected", "false") + }) +}) diff --git a/frontends/mit-open/src/pages/FieldPage/FieldSearch.tsx b/frontends/mit-open/src/pages/FieldPage/FieldSearch.tsx new file mode 100644 index 0000000000..4d4d9dc268 --- /dev/null +++ b/frontends/mit-open/src/pages/FieldPage/FieldSearch.tsx @@ -0,0 +1,211 @@ +import React, { useCallback, useMemo } from "react" +import { + styled, + SearchInput, + Pagination, + Card, + CardContent, +} from "ol-components" +import { getReadableResourceType } from "ol-utilities" + +import { + ResourceTypeEnum, + LearningResourcePlatform, + LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as LRSearchRequest, +} from "api" +import { + useLearningResourcesSearch, + usePlatformsList, +} from "api/hooks/learningResources" + +import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" +import { + useResourceSearchParams, + UseResourceSearchParamsProps, +} from "@mitodl/course-search-utils" +import type { Facets } from "@mitodl/course-search-utils" +import { useSearchParams } from "@mitodl/course-search-utils/react-router" +import LearningResourceCard from "@/page-components/LearningResourceCard/LearningResourceCard" +import CardRowList from "@/components/CardRowList/CardRowList" +import _ from "lodash" +import AvailableFacetsDropdowns from "./FieldSearchFacetDisplay" +import type { FacetManifest } from "./FieldSearchFacetDisplay" +import { getLastPage } from "../SearchPage/SearchPage" +const getFacetManifest = ( + platforms: Record, + constantSearchParams: Facets, +): FacetManifest => { + return [ + { + name: "resource_type", + title: "Learning Resource", + labelFunction: (key: string) => + getReadableResourceType(key as ResourceTypeEnum) || key, + }, + { + name: "topic", + title: "Topics", + }, + { + name: "platform", + title: "Platforn", + labelFunction: (key: string) => platforms[key]?.name ?? key, + }, + ].filter((facetSetting) => !(facetSetting.name in constantSearchParams)) +} +const FACET_NAMES = getFacetManifest({}, {}).map( + (f) => f.name, +) as UseResourceSearchParamsProps["facets"] + +const SearchField = styled(SearchInput)` + background-color: ${({ theme }) => theme.custom.colorBackgroundLight}; + width: 100%; + margin-top: 9px; +` + +const PaginationContainer = styled.div` + display: flex; + justify-content: end; +` + +export const FieldSearchControls = styled.div` + position: relative; + flex-grow: 0.95; + justify-content: flex-end; + min-height: 38px; + display: flex; + align-items: center; + margin-bottom: 30px; + margin-top: 30px; +` + +const PAGE_SIZE = 10 + +interface FeildSearchProps { + constantSearchParams: Facets +} + +const FieldSearch: React.FC = ({ constantSearchParams }) => { + const useFacetManifest = () => { + const platformsQuery = usePlatformsList() + + const platforms = useMemo(() => { + return _.keyBy(platformsQuery.data?.results ?? [], (p) => p.code) + }, [platformsQuery.data?.results]) + const facetManifest = useMemo( + () => getFacetManifest(platforms, constantSearchParams), + [platforms], + ) + return facetManifest + } + + const [searchParams, setSearchParams] = useSearchParams() + + const setPage = useCallback( + (newPage: number) => { + setSearchParams((current) => { + const copy = new URLSearchParams(current) + if (newPage === 1) { + copy.delete("page") + } else { + copy.set("page", newPage.toString()) + } + return copy + }) + }, + [setSearchParams], + ) + + const onFacetsChange = useCallback(() => { + setPage(1) + }, [setPage]) + + const { + params, + toggleParamValue, + currentText, + setCurrentText, + setCurrentTextAndQuery, + } = useResourceSearchParams({ + searchParams, + setSearchParams, + facets: FACET_NAMES, + onFacetsChange, + }) + + const allParams = useMemo(() => { + return { ...constantSearchParams, ...params } + }, [params, constantSearchParams]) + + const facetManifest = useFacetManifest() + + const page = +(searchParams.get("page") ?? "1") + + const { data } = useLearningResourcesSearch( + { + ...(allParams as LRSearchRequest), + aggregations: FACET_NAMES as LRSearchRequest["aggregations"], + offset: (page - 1) * PAGE_SIZE, + }, + { keepPreviousData: false }, + ) + + return ( + <> + + + + data?.metadata.aggregations?.[name] ?? []} + constantSearchParams={constantSearchParams} + /> + + + setCurrentText(e.target.value)} + onSubmit={(e) => { + setCurrentTextAndQuery(e.target.value) + }} + onClear={() => { + setCurrentTextAndQuery("") + }} + placeholder="" + /> + + + +
+ {data && data.count > 0 ? ( + + {data.results.map((resource) => ( +
  • + +
  • + ))} +
    + ) : ( + + No results found for your query. + + )} + + setPage(newPage)} + /> + +
    + + ) +} + +export default FieldSearch diff --git a/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx b/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx new file mode 100644 index 0000000000..43256e5eb9 --- /dev/null +++ b/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx @@ -0,0 +1,130 @@ +import React from "react" +import type { + Aggregation, + Bucket, + Facets, + FacetKey, +} from "@mitodl/course-search-utils" +import { FormControl, Select, MenuItem, SelectChangeEvent } from "ol-components" +export type KeyWithLabel = { key: string; label: string } + +export type SingleFacetOptions = { + name: string + title: string + labelFunction?: ((value: string) => string) | null +} + +export type FacetManifest = SingleFacetOptions[] + +interface FacetDisplayProps { + facetMap: FacetManifest + /** + * Returns the aggregation options for a given group. + * + * If `activeFacets` includes a facet with no results, that facet will + * automatically be included in the facet options. + */ + facetOptions: (group: string) => Aggregation | null + activeFacets: Facets + clearAllFilters: () => void + onFacetChange: (name: string, value: string, isEnabled: boolean) => void + constantSearchParams: Facets +} + +const filteredResultsWithLabels = ( + results: Aggregation, + labelFunction: ((value: string) => string) | null | undefined, + constantsForFacet: string[] | null, +): KeyWithLabel[] => { + const newResults = [] as KeyWithLabel[] + if (constantsForFacet) { + constantsForFacet.map((key: string) => { + newResults.push({ + key: key, + label: labelFunction ? labelFunction(key) : key, + }) + }) + } else { + results.map((singleFacet: Bucket) => { + newResults.push({ + key: singleFacet.key, + label: labelFunction ? labelFunction(singleFacet.key) : singleFacet.key, + }) + }) + } + + return newResults +} + +const humanize = (key: string) => { + return key.replace("_", " ") +} + +const AvailableFacetsDropdowns: React.FC< + Omit +> = ({ + facetMap, + facetOptions, + activeFacets, + onFacetChange, + constantSearchParams, +}) => { + const getHandleChangeForFacet = (active: string[], facetName: string) => { + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event + + for (const selected of value) { + if (!(active || []).includes(selected)) { + onFacetChange(facetName, selected, true) + } + } + + for (const current of active || []) { + if (!value.includes(current)) { + onFacetChange(facetName, current, false) + } + } + } + + return handleChange + } + + return ( + <> + {facetMap.map((facetSetting) => ( + + + + ))} + + ) +} + +export default AvailableFacetsDropdowns diff --git a/frontends/mit-open/src/pages/SearchPage/SearchPage.tsx b/frontends/mit-open/src/pages/SearchPage/SearchPage.tsx index 53b81b62c4..d412684651 100644 --- a/frontends/mit-open/src/pages/SearchPage/SearchPage.tsx +++ b/frontends/mit-open/src/pages/SearchPage/SearchPage.tsx @@ -210,7 +210,8 @@ const PaginationContainer = styled.div` const PAGE_SIZE = 10 const MAX_PAGE = 50 -const getLastPage = (count: number): number => { + +export const getLastPage = (count: number): number => { const pages = Math.ceil(count / PAGE_SIZE) return pages > MAX_PAGE ? MAX_PAGE : pages } diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index 146e91f8d7..be7be158ea 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -58,6 +58,7 @@ export { default as Grid } from "@mui/material/Grid" export type { GridProps } from "@mui/material/Grid" export { default as IconButton } from "@mui/material/IconButton" export type { IconButtonProps } from "@mui/material/IconButton" +export { default as InputLabel } from "@mui/material/InputLabel" export { default as List } from "@mui/material/List" export type { ListProps } from "@mui/material/List" @@ -67,6 +68,10 @@ export { default as ListItemButton } from "@mui/material/ListItemButton" export type { ListItemButtonProps } from "@mui/material/ListItemButton" export { default as ListItemText } from "@mui/material/ListItemText" export type { ListItemTextProps } from "@mui/material/ListItemText" +export { default as OutlinedInput } from "@mui/material/OutlinedInput" + +export { default as Select } from "@mui/material/Select" +export type { SelectChangeEvent } from "@mui/material/Select" export { default as Skeleton } from "@mui/material/Skeleton" export type { SkeletonProps } from "@mui/material/Skeleton"