-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
700 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.