Skip to content

Commit

Permalink
move filters to course search utils
Browse files Browse the repository at this point in the history
  • Loading branch information
abeglova committed Feb 22, 2024
1 parent 4d45f3c commit 20c7ef9
Show file tree
Hide file tree
Showing 18 changed files with 700 additions and 26 deletions.
Binary file added .DS_Store
Binary file not shown.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
"homepage": "https://github.com/mitodl/course-search-utils#readme",
"dependencies": {
"axios": "^1.6.7",
"fuse.js": "^7.0.0",
"lodash.uppercase": "^4.3.0",
"query-string": "^6.13.1",
"ramda": "^0.27.1"
"ramda": "^0.27.1",
"react-dotdotdot": "^1.3.1"
},
"devDependencies": {
"@swc/core": "^1.3.0",
Expand All @@ -44,6 +47,7 @@
"@types/enzyme": "^3.10.7",
"@types/jest": "^29.0.1",
"@types/lodash": "^4.14.162",
"@types/lodash.uppercase": "^4.3.9",
"@types/node": "^14.6",
"@types/ramda": "^0.27.27",
"@types/react": "^16.9.49",
Expand Down
Binary file added src/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Facets } from "./url_utils"
import type { Facets } from "./facet_display/types"

export enum LearningResourceType {
Course = "course",
Expand Down
Binary file added src/facet_display/.DS_Store
Binary file not shown.
74 changes: 74 additions & 0 deletions src/facet_display/Facet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useState } from "react"
import { contains } from "ramda"

import { SearchFacetItem } from "./SearchFacetItem"
import { BucketWithLabel } from "./FacetDisplay"

const MAX_DISPLAY_COUNT = 5
const FACET_COLLAPSE_THRESHOLD = 15

interface Props {
name: string
title: string
results: BucketWithLabel[] | null
currentlySelected: string[]
onUpdate: React.ChangeEventHandler<HTMLInputElement>
expandedOnLoad: boolean
}

function SearchFacet(props: Props) {
const { name, title, results, currentlySelected, onUpdate, expandedOnLoad } =
props

const [showFacetList, setShowFacetList] = useState(expandedOnLoad)
const [showAllFacets, setShowAllFacets] = useState(false)

const titleLineIcon = showFacetList ? "arrow_drop_down" : "arrow_right"

return results && results.length === 0 ? null : (
<div className="facets mb-3">
<button
className="filter-section-button pl-3 py-2 pr-0"
type="button"
aria-expanded={showFacetList ? "true" : "false"}
onClick={() => setShowFacetList(!showFacetList)}
>
{title}
<i className={`material-icons ${titleLineIcon}`} aria-hidden="true">
{titleLineIcon}
</i>
</button>
{showFacetList ? (
<React.Fragment>
{results ?
results.map((facet, i) =>
showAllFacets ||
i < MAX_DISPLAY_COUNT ||
results.length < FACET_COLLAPSE_THRESHOLD ? (
<SearchFacetItem
key={i}
facet={facet}
isChecked={contains(facet.key, currentlySelected || [])}
onUpdate={onUpdate}
name={name}
displayKey={facet.label ? facet.key : facet.key}
/>
) : null
) :
null}
{results && results.length >= FACET_COLLAPSE_THRESHOLD ? (
<button
className="facet-more-less-button"
onClick={() => setShowAllFacets(!showAllFacets)}
type="button"
>
{showAllFacets ? "View less" : "View more"}
</button>
) : null}
</React.Fragment>
) : null}
</div>
)
}

export default SearchFacet
119 changes: 119 additions & 0 deletions src/facet_display/FacetDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from "react"
import { shallow } from "enzyme"

import {
default as FacetDisplay,
getDepartmentName,
getLevelName
} from "./FacetDisplay"
import { FacetManifest, Facets } from "./types"

describe("FacetDisplay component", () => {
const facetMap: FacetManifest = [
{
name: "topic",
title: "Topics",
useFilterableFacet: false,
expandedOnLoad: false
},
{
name: "resource_type",
title: "Types",
useFilterableFacet: false,
expandedOnLoad: false
},
{
name: "department",
title: "Departments",
useFilterableFacet: false,
expandedOnLoad: true,
labelFunction: getDepartmentName
},
{
name: "level",
title: "Level",
useFilterableFacet: false,
expandedOnLoad: true,
labelFunction: getLevelName
}
]

function setup() {
const activeFacets = {}
const facetOptions = jest.fn()
const onUpdateFacets = jest.fn()
const clearAllFilters = jest.fn()
const toggleFacet = jest.fn()

const render = (props = {}) =>
shallow(
<FacetDisplay
facetMap={facetMap}
facetOptions={facetOptions}
activeFacets={activeFacets}
onUpdateFacets={onUpdateFacets}
clearAllFilters={clearAllFilters}
toggleFacet={toggleFacet}
{...props}
/>
)
return { render, clearAllFilters }
}

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) => {
expect(facet.prop("name")).toBe(facetMap[key].name)
expect(facet.prop("title")).toBe(facetMap[key].title)
expect(facet.prop("expandedOnLoad")).toBe(facetMap[key].expandedOnLoad)
})
})

test("shows filters which are active", () => {
const activeFacets: Facets = {
topic: ["Academic Writing", "Accounting", "Aerodynamics"],
resource_type: [],
department: ["1", "2"]
}

const { render, clearAllFilters } = setup()
const wrapper = render({
activeFacets
})
expect(
wrapper
.find(".active-search-filters")
.find("SearchFilter")
.map(el => el.prop("value"))
).toEqual(["Academic Writing", "Accounting", "Aerodynamics", "1", "2"])
wrapper.find(".clear-all-filters-button").simulate("click")
expect(clearAllFilters).toHaveBeenCalled()
})

test("it accepts a label function to convert codes to names", () => {
const activeFacets: Facets = {
topic: [],
resource_type: [],
department: ["1"],
level: ["graduate"]
}

const { render, clearAllFilters } = setup()
const wrapper = render({
activeFacets
})
expect(
wrapper
.find(".active-search-filters")
.find("SearchFilter")
.map(el =>
el.render().find(".active-search-filter-label").first().html()
)
).toEqual(["Civil and Environmental Engineering", "Graduate"])
wrapper.find(".clear-all-filters-button").simulate("click")
expect(clearAllFilters).toHaveBeenCalled()
})
})
137 changes: 137 additions & 0 deletions src/facet_display/FacetDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from "react"
import FilterableFacet from "./FilterableFacet"
import Facet from "./Facet"
import SearchFilter from "./SearchFilter"
import type { FacetManifest, Facets, Aggregation, Bucket } from "./types"
import { LEVELS, DEPARTMENTS } from "../constants"

export type BucketWithLabel = Bucket & { label: string | null }

interface Props {
facetMap: FacetManifest
facetOptions: (group: string) => Aggregation | null
activeFacets: Facets
onUpdateFacets: React.ChangeEventHandler<HTMLInputElement>
clearAllFilters: () => void
toggleFacet: (name: string, value: string, isEnabled: boolean) => void
}

export const getDepartmentName = (departmentId: string): string | null => {
if (departmentId in DEPARTMENTS) {
return DEPARTMENTS[departmentId as keyof typeof DEPARTMENTS]
} else {
return departmentId
}
}

export const getLevelName = (levelValue: string): string | null => {
if (levelValue in LEVELS) {
return LEVELS[levelValue as keyof typeof LEVELS]
} else {
return levelValue
}
}

const resultsWithLabels = (
results: Aggregation | null,
labelFunction: ((value: string) => string | null) | null | undefined
): BucketWithLabel[] => {
const newResults = [] as BucketWithLabel[]
;(results || []).map((singleFacet: Bucket) => {
if (labelFunction) {
newResults.push({
key: singleFacet.key,
doc_count: singleFacet.doc_count,
label: labelFunction(singleFacet.key)
})
} else {
newResults.push({
key: singleFacet.key,
doc_count: singleFacet.doc_count,
label: null
})
}
})

return newResults
}

const FacetDisplay = React.memo(
function FacetDisplay(props: Props) {
const {
facetMap,
facetOptions,
activeFacets,
onUpdateFacets,
clearAllFilters,
toggleFacet
} = props

return (
<React.Fragment>
<div className="active-search-filters">
<div className="filter-section-main-title">
Filters
<button
className="clear-all-filters-button"
type="button"
onClick={clearAllFilters}
>
Clear All
</button>
</div>
{facetMap.map(facetSetting =>
(activeFacets[facetSetting.name] || []).map((facet, i) => (
<SearchFilter
key={i}
value={facet}
clearFacet={() => toggleFacet(facetSetting.name, facet, false)}
labelFunction={facetSetting.labelFunction || null}
/>
))
)}
</div>
{facetMap.map((facetSetting, key) =>
facetSetting.useFilterableFacet ? (
<FilterableFacet
key={key}
results={resultsWithLabels(
facetOptions(facetSetting.name),
facetSetting.labelFunction
)}
name={facetSetting.name}
title={facetSetting.title}
currentlySelected={activeFacets[facetSetting.name] || []}
onUpdate={onUpdateFacets}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
) : (
<Facet
key={key}
title={facetSetting.title}
name={facetSetting.name}
results={resultsWithLabels(
facetOptions(facetSetting.name),
facetSetting.labelFunction
)}
onUpdate={onUpdateFacets}
currentlySelected={activeFacets[facetSetting.name] || []}
expandedOnLoad={facetSetting.expandedOnLoad}
/>
)
)}
</React.Fragment>
)
},
(prevProps, nextProps) => {
return (
prevProps.activeFacets === nextProps.activeFacets &&
prevProps.clearAllFilters === nextProps.clearAllFilters &&
prevProps.toggleFacet === nextProps.toggleFacet &&
prevProps.facetOptions === nextProps.facetOptions &&
prevProps.onUpdateFacets === nextProps.onUpdateFacets
)
}
)

export default FacetDisplay
Loading

0 comments on commit 20c7ef9

Please sign in to comment.