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

Xstate fixes #63

Merged
merged 15 commits into from
Nov 28, 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
145 changes: 113 additions & 32 deletions __tests__/search.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nice with some test on the facets!

Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { ReadonlyURLSearchParams } from "next/navigation"
import { beforeEach, describe, expect, it, vi } from "vitest"

import {
getFacetsForSearchRequest,
getSearchQueryArguments,
} from "@/components/pages/searchPageLayout/helper"
import { getFacetMachineNames, getFacetTranslation } from "@/components/shared/searchFilters/helper"
import goConfig from "@/lib/config/goConfig"
import { FacetFieldEnum } from "@/lib/graphql/generated/fbi/graphql"
import { correctFacetNames, transformSearchParamsIntoFilters } from "@/lib/machines/search/helpers"

vi.mock(import("@/lib/config/goConfig"), async importOriginal => {
const actual = await importOriginal()
Expand Down Expand Up @@ -62,7 +59,7 @@ describe("Facet functionality", () => {
])
})

it("getFacetsForSearchRequest should return an object with facet terms grouped by facet machine names", () => {
it("transformSearchParamsIntoFilters should return an object with facet terms grouped by facet machine names", () => {
const searchParams = new URLSearchParams()
searchParams.append("materialTypesGeneral", "Book")
searchParams.append("mainLanguages", "Danish")
Expand All @@ -81,38 +78,122 @@ describe("Facet functionality", () => {
subjects: ["Science", "Math"],
}

const result = getFacetsForSearchRequest(searchParams as ReadonlyURLSearchParams)
const result = transformSearchParamsIntoFilters(searchParams as ReadonlyURLSearchParams)
expect(result).toStrictEqual(facetFilters)
})

it("getSearchQueryArguments should return an object with search query arguments", () => {
const facetFilters = {
materialTypesGeneral: ["Book"],
mainLanguages: ["Danish", "English"],
age: ["Adult"],
lixRange: ["0-10", "11-20"],
subjects: ["Science", "Math"],
}
it("getFacetTranslation should give a translated facet when given a facet machine name", () => {
const translation = getFacetTranslation("lixRange")
expect(translation).toBe("Lix")
})

const result = getSearchQueryArguments({ q: "Harry Potter", currentPage: 0, facetFilters })
expect(result).toStrictEqual({
q: { all: "Harry Potter" },
offset: 0,
limit: 9,
filters: {
branchId: ["11", "22", "33"],
materialTypesGeneral: ["Book"],
mainLanguages: ["Danish", "English"],
age: ["Adult"],
lixRange: ["0-10", "11-20"],
subjects: ["Science", "Math"],
it("correctFacetNames should translate the facet names to input filter valid names", () => {
const facets = [
{
name: "materialTypesGeneral",
values: [
{
key: "podcasts",
term: "podcasts",
score: 179,
},
],
},
})
})
{
name: "mainLanguages",
values: [
{
key: "dan",
term: "Dansk",
score: 238,
},
],
},
{
name: "age",
values: [
{
key: "for 10 år",
term: "for 10 år",
score: 25,
},
],
},
{
name: "lix",
values: [
{
key: "22",
term: "22",
score: 2,
},
],
},
{
name: "subjects",
values: [
{
key: "magi",
term: "magi",
score: 192,
},
],
},
]

it("getFacetTranslation should give a translated facet when given a facet machine name", () => {
// @ts-ignore
const translation = getFacetTranslation("LIX")
expect(translation).toBe("Lix")
const result = correctFacetNames(facets)

expect(result).toStrictEqual([
{
name: "materialTypesGeneral",
values: [
{
key: "podcasts",
term: "podcasts",
score: 179,
},
],
},
{
name: "mainLanguages",
values: [
{
key: "dan",
term: "Dansk",
score: 238,
},
],
},
{
name: "age",
values: [
{
key: "for 10 år",
term: "for 10 år",
score: 25,
},
],
},
{
name: "lixRange",
values: [
{
key: "22",
term: "22",
score: 2,
},
],
},
{
name: "subjects",
values: [
{
key: "magi",
term: "magi",
score: 192,
},
],
},
])
})
})
12 changes: 9 additions & 3 deletions components/pages/searchPageLayout/SearchPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ const SearchPageLayout = () => {
const loadMoreRef = useRef(null)
const isInView = useInView(loadMoreRef)
const actor = useSearchMachineActor()
const { data, isLoadingFacets, isLoadingResults, machineIsReady, searchQuery } =
useSearchDataAndLoadingStates()
const {
data,
isLoadingFacets,
isLoadingResults,
isLoadingMoreResults,
machineIsReady,
searchQuery,
} = useSearchDataAndLoadingStates()

useEffect(() => {
if (isInView) {
Expand Down Expand Up @@ -75,7 +81,7 @@ const SearchPageLayout = () => {
</motion.div>
)
)}
{isLoadingResults && <SearchResultsGhost />}
{isLoadingMoreResults && <SearchResultsGhost />}
</div>
</>
) : (
Expand Down
65 changes: 5 additions & 60 deletions components/pages/searchPageLayout/helper.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,7 @@
import { GetNextPageParamFunction } from "@tanstack/react-query"
import { useSelector } from "@xstate/react"
import { ReadonlyURLSearchParams } from "next/navigation"

import { getFacetMachineNames } from "@/components/shared/searchFilters/helper"
import goConfig from "@/lib/config/goConfig"
import { SearchFiltersInput, SearchWithPaginationQuery } from "@/lib/graphql/generated/fbi/graphql"
import { TFilters } from "@/lib/machines/search/types"
import useSearchMachineActor from "@/lib/machines/search/useSearchMachineActor"

export const getSearchQueryArguments = ({
q,
currentPage,
facetFilters,
}: {
q: string
currentPage: number
facetFilters: SearchFiltersInput
}) => {
const limit = goConfig("search.item.limit")
return {
q: { all: q },
offset: currentPage * limit,
limit: limit,
filters: {
branchId: goConfig("search.branch.ids"),
...facetFilters,
},
}
}

export const getFacetsForSearchRequest = (searchParams: ReadonlyURLSearchParams) => {
const facets = goConfig("search.facets")
const facetsMachineNames = getFacetMachineNames()

return facetsMachineNames.reduce(
(acc: TFilters, machineName) => {
const values = searchParams.getAll(facets[machineName as keyof typeof facets].filter)
if (values.length > 0) {
return {
...acc,
[facets[machineName as keyof typeof facets].filter]: [...values],
}
}
return acc
},
{} as { [key: string]: keyof TFilters[] }
)
}

export const getNextPageParamsFunc = (
currentPage: number
): GetNextPageParamFunction<number, SearchWithPaginationQuery> => {
const limit = goConfig("search.item.limit")

return ({ search: { hitcount } }) => {
const totalPages = Math.ceil(hitcount / limit)
const nextPage = currentPage + 1
return currentPage < totalPages ? nextPage : undefined // By returning undefined if there are no more pages, hasNextPage boolean will be set to false
}
}

export const useSearchDataAndLoadingStates = () => {
const actor = useSearchMachineActor()
const searchQuery = useSelector(actor, snapshot => {
Expand All @@ -71,8 +13,10 @@ export const useSearchDataAndLoadingStates = () => {
})
const isLoadingFacets =
!data.facets || actor.getSnapshot().matches({ filteringAndSearching: "filter" })
const isLoadingResults =
!data.search || actor.getSnapshot().matches({ filteringAndSearching: "search" })
const isLoadingResults = actor.getSnapshot().matches({ filteringAndSearching: "search" })
const isLoadingMoreResults = actor
.getSnapshot()
.matches({ loadingMoreSearchResults: "searching" })
const machineIsReady = !actor.getSnapshot().matches("bootstrap")

const selectedFilters = useSelector(actor, snapshot => snapshot.context.selectedFilters)
Expand All @@ -83,6 +27,7 @@ export const useSearchDataAndLoadingStates = () => {
selectedFilters,
isLoadingFacets,
isLoadingResults,
isLoadingMoreResults,
machineIsReady,
}
}
38 changes: 20 additions & 18 deletions components/shared/searchFilters/SearchFiltersColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AnimateChangeInHeight } from "@/components/shared/animateChangeInHeight
import BadgeButton from "@/components/shared/badge/BadgeButton"
import Icon from "@/components/shared/icon/Icon"
import {
createToggleFilterCallback,
facetTermIsSelected,
getFacetTranslation,
sortByActiveFacets,
Expand All @@ -17,36 +18,39 @@ import useSearchMachineActor from "@/lib/machines/search/useSearchMachineActor"
type SearchFiltersColumnProps = {
facet: SearchFacetFragment
isLast: boolean
isExpanded: boolean
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>
}

const SearchFiltersColumn = ({
facet,
isLast,
isExpanded,
setIsExpanded,
}: SearchFiltersColumnProps) => {
const SearchFiltersColumn = ({ facet, isLast }: SearchFiltersColumnProps) => {
const actor = useSearchMachineActor()
const [isExpanded, setIsExpanded] = useState<boolean>(false)
const facetFilter = facet.name as keyof TFilters
const elementRef = useRef<HTMLDivElement | null>(null)
const [hasOverflow, setHasOverflow] = useState(false)
const { selectedFilters } = useSearchDataAndLoadingStates()
const toggleFilter = createToggleFilterCallback(actor)
const facetData = actor.getSnapshot().context.facetData

// We show the selected values first in the list
if (selectedFilters) {
facet.values = sortByActiveFacets(facet, selectedFilters)
}

useEffect(() => {
const el = elementRef.current
if (el) {
const isOverflowing = el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth
const isOverflowing = el.scrollHeight > el.clientHeight

if (isOverflowing) {
setHasOverflow(true)
} else {
setHasOverflow(false)
}
}
}, [elementRef])
}, [elementRef?.current?.scrollHeight])

// We show the selected values first in the list
if (selectedFilters) {
facet.values = sortByActiveFacets(facet, selectedFilters)
}
useEffect(() => {
setIsExpanded(false)
}, [facetData])

return (
<>
Expand All @@ -73,9 +77,7 @@ const SearchFiltersColumn = ({
<BadgeButton
key={index}
ariaLabel={value.term}
onClick={() =>
actor.send({ type: "TOGGLE_FILTER", name: facet.name, value: value.term })
}
onClick={() => toggleFilter({ name: facet.name, value: value.term })}
isActive={facetTermIsSelected({
facet: facet.name,
term: value.term,
Expand All @@ -88,7 +90,7 @@ const SearchFiltersColumn = ({
{hasOverflow && (
<BadgeButton
ariaLabel={isExpanded ? "Vis færre" : "Vis flere"}
classNames={cn(`pl-3 w-auto flex flex-row items-center self-start ml-1`)}
classNames={cn(`pl-3 w-auto flex flex-row items-center self-start mt-1`)}
onClick={() => {
setIsExpanded(prev => !prev)
}}>
Expand Down
15 changes: 2 additions & 13 deletions components/shared/searchFilters/SearchFiltersDesktop.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
"use client"

import React, { Suspense, useState } from "react"
import React from "react"

import { SearchFacetFragment } from "@/lib/graphql/generated/fbi/graphql"

import SearchFiltersColumn, { SearchFiltersColumnGhost } from "./SearchFiltersColumn"

const SearchFiltersDesktop = ({ facets }: { facets: SearchFacetFragment[] }) => {
const [isExpanded, setIsExpanded] = useState<boolean>(false)

return (
<div className="flex flex-row gap-4">
{facets.map((facet, index) => {
const isLast = index === facets.length - 1
return (
<Suspense key={facet.name} fallback={<p>Loading...</p>}>
<SearchFiltersColumn
facet={facet}
isLast={isLast}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
/>
</Suspense>
)
return <SearchFiltersColumn key={index} facet={facet} isLast={isLast} />
})}
</div>
)
Expand Down
Loading