From d278566ac648dcf0eaa5f52e9da209199d18755c Mon Sep 17 00:00:00 2001 From: Mounir Dhahri Date: Sun, 15 Oct 2023 20:38:28 +0200 Subject: [PATCH] feat: add price range filter to saved searches (#9415) * feat: add price range filter to saved searches * chore: add tests * chore: self review * chore: improve skeleton * chore: address review comment * fix: test, again --- .../PriceRange/PriceRangeContainer.tsx | 5 +- .../SavedSearchFilterPriceRange.tests.tsx | 77 ++++++++++ .../SavedSearchFilterPriceRange.tsx | 132 ++++++++++++++++++ .../screens/SavedSearchFilterScreen.tsx | 14 +- 4 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 src/app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange.tests.tsx create mode 100644 src/app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange.tsx diff --git a/src/app/Components/PriceRange/PriceRangeContainer.tsx b/src/app/Components/PriceRange/PriceRangeContainer.tsx index 15f5ceccbc1..f4cc3523581 100644 --- a/src/app/Components/PriceRange/PriceRangeContainer.tsx +++ b/src/app/Components/PriceRange/PriceRangeContainer.tsx @@ -23,7 +23,7 @@ interface RecentPriceRangeEntity { interface PriceRangeContainerProps { filterPriceRange: string // *-* histogramBars: HistogramBarEntity[] - header: React.ReactNode + header?: React.ReactNode onPriceRangeUpdate: (range: PriceRange) => void onRecentPriceRangeSelected?: (isCollectorProfileSources: boolean) => void } @@ -103,7 +103,8 @@ export const PriceRangeContainer: React.FC = ({ return ( - {header} + {!!header && {header}} + { + it("shows the right price range when available", () => { + const { renderWithRelay } = setupTestWrapper({ + Component: () => ( + + + + ), + }) + + const { getByText } = renderWithRelay({ + Artist: () => ({ + internalID: "artistID", + name: "Banksy", + }), + }) + + waitFor(() => { + expect(getByText("200")).toBeDefined() + expect(getByText("3000")).toBeDefined() + }) + }) + + it("Updates the price range appropriately", async () => { + const { renderWithRelay } = setupTestWrapper({ + Component: () => ( + + + + ), + }) + + const { getByTestId, getByText } = renderWithRelay({ + Artist: () => ({ + internalID: "artistID", + name: "Banksy", + }), + }) + + waitFor(() => { + expect(getByText("200")).toBeDefined() + expect(getByText("3000")).toBeDefined() + + fireEvent.changeText(getByTestId("price-min-input"), "300") + fireEvent.changeText(getByTestId("price-max-input"), "5000") + + expect(getByText("300")).toBeDefined() + expect(getByText("5000")).toBeDefined() + }) + }) +}) + +const initialData: SavedSearchModel = { + ...savedSearchModel, + attributes: { + atAuction: true, + priceRange: "200-3000", + }, + entity: { + artists: [{ id: "artistID", name: "Banksy" }], + owner: { + type: OwnerType.artist, + id: "ownerId", + slug: "ownerSlug", + }, + }, +} diff --git a/src/app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange.tsx b/src/app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange.tsx new file mode 100644 index 00000000000..8a4067cecd6 --- /dev/null +++ b/src/app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange.tsx @@ -0,0 +1,132 @@ +import { Flex, Skeleton, SkeletonBox, SkeletonText, Spacer, Text } from "@artsy/palette-mobile" +import { SavedSearchFilterPriceRangeQuery } from "__generated__/SavedSearchFilterPriceRangeQuery.graphql" +import { SearchCriteria } from "app/Components/ArtworkFilter/SavedSearch/types" +import { PriceRangeContainer } from "app/Components/PriceRange/PriceRangeContainer" +import { DEFAULT_PRICE_RANGE } from "app/Components/PriceRange/constants" +import { PriceRange } from "app/Components/PriceRange/types" +import { getBarsFromAggregations } from "app/Components/PriceRange/utils" +import { SavedSearchStore } from "app/Scenes/SavedSearchAlert/SavedSearchStore" +import { useSearchCriteriaAttributes } from "app/Scenes/SavedSearchAlert/helpers" +import { GlobalStore } from "app/store/GlobalStore" +import { withSuspense } from "app/utils/hooks/withSuspense" +import { useEffect, useState } from "react" +import { graphql, useLazyLoadQuery } from "react-relay" +import useDebounce from "react-use/lib/useDebounce" + +interface SavedSearchFilterPriceRangeProps { + artist: SavedSearchFilterPriceRangeQuery["response"]["artist"] +} + +const SavedSearchFilterPriceRange: React.FC = ({ artist }) => { + const histogramBars = getBarsFromAggregations( + (artist as any)?.filterArtworksConnection?.aggregations + ) + + const storePriceRangeValue = useSearchCriteriaAttributes(SearchCriteria.priceRange) as string + + const [filterPriceRange, setFilterPriceRange] = useState( + storePriceRangeValue || DEFAULT_PRICE_RANGE + ) + + const setValueToAttributesByKeyAction = SavedSearchStore.useStoreActions( + (actions) => actions.setValueToAttributesByKeyAction + ) + + useDebounce( + () => { + setValueToAttributesByKeyAction({ + key: SearchCriteria.priceRange, + value: filterPriceRange, + }) + GlobalStore.actions.recentPriceRanges.addNewPriceRange(filterPriceRange) + }, + 200, + [filterPriceRange] + ) + + // Make sure to keep the slider and the histograms up to date with the store + useEffect(() => { + if (filterPriceRange !== storePriceRangeValue) { + setFilterPriceRange(storePriceRangeValue || DEFAULT_PRICE_RANGE) + } + }, [storePriceRangeValue]) + + const handleUpdateRange = (updatedRange: PriceRange) => { + setFilterPriceRange(updatedRange.join("-")) + } + + return ( + + + Price Range + + + + + ) +} + +const Placeholder: React.FC<{}> = () => ( + + + + Price Range + + + + + + + + + + + + + + + + + + + + + +) + +const savedSearchFilterPriceRangeQuery = graphql` + query SavedSearchFilterPriceRangeQuery($artistID: String!) { + artist(id: $artistID) { + filterArtworksConnection(aggregations: [SIMPLE_PRICE_HISTOGRAM], first: 0) { + aggregations { + slice + counts { + count + name + value + } + } + } + } + } +` + +export const SavedSearchFilterPriceRangeQR: React.FC<{}> = withSuspense(() => { + const artistID = SavedSearchStore.useStoreState((state) => state.entity.artists[0].id) + const data = useLazyLoadQuery( + savedSearchFilterPriceRangeQuery, + { + artistID: artistID, + } + ) + + if (!data.artist) { + return null + } + + return +}, Placeholder) diff --git a/src/app/Scenes/SavedSearchAlert/screens/SavedSearchFilterScreen.tsx b/src/app/Scenes/SavedSearchAlert/screens/SavedSearchFilterScreen.tsx index 70f04f711b9..f0b848ce590 100644 --- a/src/app/Scenes/SavedSearchAlert/screens/SavedSearchFilterScreen.tsx +++ b/src/app/Scenes/SavedSearchAlert/screens/SavedSearchFilterScreen.tsx @@ -1,19 +1,20 @@ -import { Flex, Join, Separator, Text, Touchable } from "@artsy/palette-mobile" +import { Join, Separator, Text, Touchable } from "@artsy/palette-mobile" import { useNavigation } from "@react-navigation/native" import { SearchCriteria } from "app/Components/ArtworkFilter/SavedSearch/types" import { FancyModalHeader } from "app/Components/FancyModal/FancyModalHeader" import { SavedSearchAppliedFilters } from "app/Scenes/SavedSearchAlert/Components/SavedSearchFilterAppliedFilters" import { SavedSearchFilterColour } from "app/Scenes/SavedSearchAlert/Components/SavedSearchFilterColour" +import { SavedSearchFilterPriceRangeQR } from "app/Scenes/SavedSearchAlert/Components/SavedSearchFilterPriceRange" import { SavedSearchRarity } from "app/Scenes/SavedSearchAlert/Components/SavedSearchFilterRarity" import { SavedSearchStore } from "app/Scenes/SavedSearchAlert/SavedSearchStore" import { MotiView } from "moti" -import { Alert } from "react-native" +import { Alert, ScrollView } from "react-native" export const SavedSearchFilterScreen: React.FC<{}> = () => { const navigation = useNavigation() return ( - + = () => { }> + - + ) } @@ -40,6 +42,10 @@ export const ClearAllButton = () => { Object.entries(attributes).filter((keyValue) => { const key = keyValue[0] const value = keyValue[1] + if (key === SearchCriteria.priceRange) { + return value && value !== "*-*" + } + if (key !== SearchCriteria.artistID && key !== SearchCriteria.artistIDs) { // Values might be empty arrays if (Array.isArray(value)) {