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

useResourceSearchParams and export AvailableFacets component #78

Merged
merged 5 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,42 +35,40 @@ git push --tags # and it tags it too

### Hooks

`@mitodl/course-search-utils` exports two hooks to assist in making search requests to MIT Open's APIs. They are:
`@mitodl/course-search-utils` exports a few hooks to assist in making search requests to MIT Open's APIs. They are:

1. `useSearchQueryParams({ searchParams, setSearchParams, ...opts? })`: Derive search API parameters (facets, search text, ...) from a `URLSearchParams` object. Often, the `URLSearchParams` object will be derived from the browser URL, though it could be state internal to react.
1. `useResourceSearchParams({ searchParams, setSearchParams, ...opts? })` and `useContentFileSearchParams`: Derive search API parameters from a `URLSearchParams` object. Often, the `URLSearchParams` object will be derived from the browser URL, though it could be state internal to react.

The hook extracts validated API parameters from the `URLSearchParams` object and returns setters that can be used to manipulate the `URLSearchParams` (e.g., toggling a search facet on or off).

The `URLSearchParams` keys are mapped to API parameters internally, e.g., `?d=physics&d=chemistry` maps to `departments: ["physics", "chemistry"]`. This mapping is not configurable.
The `URLSearchParams` keys are mapped directly to API parameters.

2. `useInfiniteSearch({ params, baseUrl, ...opts? })`: Assists in making search API calls used in an infinite scrolling UI. The initial page is loaded by the hook, susbsequent pages via returm value `{ fetchNextPage }`. The hook's result is based on [useInfiniteQuery](https://tanstack.com/query/v4/docs/framework/react/reference/useInfiniteQuery).
2. `useInfiniteSearch({ params, endpoint, baseUrl, ...opts? })`: Assists in making search API calls used in an infinite scrolling UI. The initial page is loaded by the hook, susbsequent pages via returm value `{ fetchNextPage }`. The hook's result is based on [useInfiniteQuery](https://tanstack.com/query/v4/docs/framework/react/reference/useInfiniteQuery).

See Typescript annotations and docstrings for more information on hook props and results. Typical usage might look like:

```tsx
import { useSearchQueryParams, useInfiniteSearch, } from "@mitodl/course-search-utils"
import type { UseInfiniteSearchProps } from "@mitodl/course-search-utils"

// set aggregations to be returned for resources and content_files endpoints
const AGGREGATIONS: UseInfiniteSearchProps["aggregations"] = {
resources: ["department", "level", "topic", "course_feature"],
content_files: ["topic", "offered_by", "content_feature_type"]
const CONSTANT_PARAMETERS = {
platform: ["ocw"],
aggregations: ["topic", "offered_by"]
}

const CONSTANT_FACETS = { platform: ["ocw"] }

const SearchPage: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
const {
params,
setFacetActive,
clearFacets,
toggleParamValue,
clearAllFacets,
currentText,
setCurrentText,
setCurrentTextAndQuery,
} = useSearchQueryParams({
searchParams,
setSearchParams,
facets: FACETS
})

// If necessary
Expand All @@ -79,9 +77,8 @@ const SearchPage: React.FC = () => {
}, [params])

const { pages, hasNextPage, fetchNextPage } = useInfiniteSearch({
params: allParams
params: allParams,
baseUrl: "http://mitopen.odl.mit.edu/",
aggregations: AGGREGATIONS,
keepPreviousData: true,
})

Expand Down
16 changes: 8 additions & 8 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { Config } from "@jest/types";
import { Config } from "@jest/types"

const common = {
testEnvironment: "jsdom",
transform: {
transform: {
"^.+\\.(t)sx?$": "@swc/jest",
},
setupFilesAfterEnv: ["<rootDir>src/test_setup.ts"],
testMatch: ["<rootDir>/src/**/*.(spec|test).ts?(x)"],
};
testMatch: ["<rootDir>/src/**/*.(spec|test).ts?(x)"],
}

const config: Config.InitialOptions = {
projects: [
{
displayName: "history-v4",
displayName: "history-v4",
moduleNameMapper: {
history$: "history-v4",
},
...common,
},
{
displayName: "history-v5",
displayName: "history-v5",
moduleNameMapper: {
history$: "history",
},
...common,
},
],
};
}

export default config;
export default config
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"homepage": "https://github.com/mitodl/course-search-utils#readme",
"dependencies": {
"@mitodl/open-api-axios": "^2024.3.22",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally, I was not going to introduce the @mitodl/open-api-axios client in this PR. However, the client previously committed in this repo was missing the new resource_type: Video which caused some type errors in MIT Open, so I went ahead and replaced the client in this PR.

"axios": "^1.6.7",
"fuse.js": "^7.0.0",
"query-string": "^6.13.1",
Expand Down
2 changes: 1 addition & 1 deletion src/facet_display/Facet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function SearchFacet(props: Props) {
i < MAX_DISPLAY_COUNT ||
results.length < FACET_COLLAPSE_THRESHOLD ? (
<SearchFacetItem
key={i}
key={`${name}-${facet.key}`}
facet={facet}
isChecked={contains(facet.key, selected || [])}
onUpdate={onUpdate}
Expand Down
7 changes: 4 additions & 3 deletions src/facet_display/FacetDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shallow } from "enzyme"

import {
default as FacetDisplay,
AvailableFacets,
getDepartmentName,
getLevelName
} from "./FacetDisplay"
Expand Down Expand Up @@ -61,9 +62,9 @@ describe("FacetDisplay component", () => {
test("renders a FacetDisplay with expected FilterableFacets", async () => {
const { render } = setup()
const wrapper = render()
const facets = wrapper.children()
expect(facets).toHaveLength(5)
facets.slice(1, 5).map((facet, key) => {
const facets = wrapper.find(AvailableFacets).dive().children()
expect(facets).toHaveLength(4)
facets.map((facet, key) => {
expect(facet.prop("name")).toBe(facetMap[key].name)
expect(facet.prop("title")).toBe(facetMap[key].title)
expect(facet.prop("expandedOnLoad")).toBe(facetMap[key].expandedOnLoad)
Expand Down
89 changes: 54 additions & 35 deletions src/facet_display/FacetDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { LEVELS, DEPARTMENTS } from "../constants"

export type BucketWithLabel = Bucket & { label: string }

interface Props {
interface FacetDisplayProps {
facetMap: FacetManifest
facetOptions: (group: string) => Aggregation | null
activeFacets: Facets
Expand Down Expand Up @@ -47,8 +47,53 @@ const resultsWithLabels = (
return newResults
}

const AvailableFacets: React.FC<Omit<FacetDisplayProps, "clearAllFilters">> = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FacetDisplay consisted over three portions:

  1. A title, "Facets" and "Clear All" button
  2. the "Active Facets", displayed in OCW as "chips" with rounded corners
  3. a list of the "Available Facets"

In MIT Open, we do not want (2). Since (1) is simple, it can be implemented easily in MIT Open.

But (3) is worth exporting on its own.

facetMap,
facetOptions,
activeFacets,
onFacetChange
}) => {
return (
<>
{facetMap.map(facetSetting =>
facetSetting.useFilterableFacet ? (
<FilterableFacet
key={facetSetting.name}
results={resultsWithLabels(
facetOptions(facetSetting.name) || [],
facetSetting.labelFunction
)}
name={facetSetting.name}
title={facetSetting.title}
selected={activeFacets[facetSetting.name] || []}
onUpdate={e =>
onFacetChange(e.target.name, e.target.value, e.target.checked)
}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
) : (
<Facet
key={facetSetting.name}
title={facetSetting.title}
name={facetSetting.name}
results={resultsWithLabels(
facetOptions(facetSetting.name) || [],
facetSetting.labelFunction
)}
onUpdate={e =>
onFacetChange(e.target.name, e.target.value, e.target.checked)
}
selected={activeFacets[facetSetting.name] || []}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
)
)}
</>
)
}

const FacetDisplay = React.memo(
function FacetDisplay(props: Props) {
function FacetDisplay(props: FacetDisplayProps) {
const {
facetMap,
facetOptions,
Expand Down Expand Up @@ -83,39 +128,12 @@ const FacetDisplay = React.memo(
))
)}
</div>
{facetMap.map((facetSetting, key) =>
facetSetting.useFilterableFacet ? (
<FilterableFacet
key={key}
results={resultsWithLabels(
facetOptions(facetSetting.name) || [],
facetSetting.labelFunction
)}
name={facetSetting.name}
title={facetSetting.title}
selected={activeFacets[facetSetting.name] || []}
onUpdate={e =>
onFacetChange(e.target.name, e.target.value, e.target.checked)
}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
) : (
<Facet
key={key}
title={facetSetting.title}
name={facetSetting.name}
results={resultsWithLabels(
facetOptions(facetSetting.name) || [],
facetSetting.labelFunction
)}
onUpdate={e =>
onFacetChange(e.target.name, e.target.value, e.target.checked)
}
selected={activeFacets[facetSetting.name] || []}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
)
)}
<AvailableFacets
facetMap={facetMap}
facetOptions={facetOptions}
activeFacets={activeFacets}
onFacetChange={onFacetChange}
/>
</>
)
},
Expand All @@ -130,3 +148,4 @@ const FacetDisplay = React.memo(
)

export default FacetDisplay
export { AvailableFacets }
4 changes: 2 additions & 2 deletions src/facet_display/FilterableFacet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ function FilterableFacet(props: Props) {
)}
</div>
<div className="facet-list">
{facets.map((facet, i) => (
{facets.map(facet => (
<SearchFacetItem
key={i}
key={`${name}-${facet.key}`}
facet={facet}
isChecked={contains(facet.key, selected || [])}
onUpdate={onUpdate}
Expand Down
130 changes: 0 additions & 130 deletions src/hooks/configs.ts

This file was deleted.

Loading
Loading