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

feat: Add Availability filter to artwork filters #11100

Merged
merged 2 commits into from
Nov 26, 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
15 changes: 13 additions & 2 deletions src/app/Components/ArtworkFilter/ArtworkFilterHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum FilterDisplayName {
artistsIFollow = "Artist",
artistSeriesIDs = "Artist Series",
attributionClass = "Rarity",
availability = "Availability",
categories = "Medium",
colors = "Color",
estimateRange = "Price/Estimate Range",
Expand Down Expand Up @@ -50,6 +51,7 @@ export enum FilterParamName {
colors = "colors",
earliestCreatedYear = "earliestCreatedYear",
estimateRange = "estimateRange",
forSale = "forSale",
height = "height",
keyword = "keyword",
latestCreatedYear = "latestCreatedYear",
Expand Down Expand Up @@ -87,6 +89,7 @@ export const QueryParamsToFilterValueMapping: Record<string, FilterParamName> =
colors: FilterParamName.colors,
earliest_created_year: FilterParamName.earliestCreatedYear,
estimate_range: FilterParamName.estimateRange,
for_sale: FilterParamName.forSale,
height: FilterParamName.height,
keyword: FilterParamName.keyword,
latest_created_year: FilterParamName.latestCreatedYear,
Expand Down Expand Up @@ -140,6 +143,7 @@ export const ParamDefaultValues = {
colors: [],
earliestCreatedYear: undefined,
estimateRange: "",
forSale: undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting the filter to undefined and not false by default. I guess both versions work, but false seems to be correct because we don't want to filter anything if the filter is not set.

height: "*-*",
includeArtworksByFollowedArtists: false,
inquireableOnly: false,
Expand Down Expand Up @@ -175,6 +179,7 @@ export const defaultCommonFilterOptions = {
colors: ParamDefaultValues.colors,
earliestCreatedYear: ParamDefaultValues.earliestCreatedYear,
estimateRange: ParamDefaultValues.estimateRange,
forSale: ParamDefaultValues.forSale,
height: ParamDefaultValues.height,
includeArtworksByFollowedArtists: ParamDefaultValues.includeArtworksByFollowedArtists,
inquireableOnly: ParamDefaultValues.inquireableOnly,
Expand Down Expand Up @@ -255,7 +260,7 @@ export interface FilterCounts {
}

export type SelectedFiltersCounts = {
[Name in FilterParamName | "waysToBuy" | "year"]: number
[Name in FilterParamName | "waysToBuy" | "year" | "availability"]: number
}

export const filterKeyFromAggregation: Record<
Expand Down Expand Up @@ -326,6 +331,8 @@ const DEFAULT_TAG_ARTWORK_PARAMS = {
sort: "-partner_updated_at",
} as FilterParams

const availabilityFilterNames = [FilterParamName.forSale]

const createdYearsFilterNames = [
FilterParamName.earliestCreatedYear,
FilterParamName.latestCreatedYear,
Expand Down Expand Up @@ -413,7 +420,7 @@ export const aggregationNameFromFilter: Record<string, AggregationName | undefin

export const aggregationForFilter = (filterKey: string, aggregations: Aggregations) => {
const aggregationName = aggregationNameFromFilter[filterKey]
const aggregation = aggregations!.find((value) => value.slice === aggregationName)
const aggregation = aggregations.find((value) => value.slice === aggregationName)
return aggregation
}

Expand Down Expand Up @@ -565,6 +572,10 @@ export const getSelectedFiltersCounts = (selectedFilters: FilterArray) => {

selectedFilters.forEach(({ paramName, paramValue }: FilterData) => {
switch (true) {
case availabilityFilterNames.includes(paramName): {
counts.availability = 1
break
}
case waysToBuyFilterNames.includes(paramName): {
counts.waysToBuy = (counts.waysToBuy ?? 0) + 1
break
Expand Down
3 changes: 3 additions & 0 deletions src/app/Components/ArtworkFilter/ArtworkFilterNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ArtistIDsOptionsScreen } from "app/Components/ArtworkFilter/Filters/Art
import { ArtistNationalitiesOptionsScreen } from "app/Components/ArtworkFilter/Filters/ArtistNationalitiesOptions"
import { ArtistSeriesOptionsScreen } from "app/Components/ArtworkFilter/Filters/ArtistSeriesOptions.tsx"
import { AttributionClassOptionsScreen } from "app/Components/ArtworkFilter/Filters/AttributionClassOptions"
import { AvailabilityOptionsScreen } from "app/Components/ArtworkFilter/Filters/AvailabilityOptions"
import { CategoriesOptionsScreen } from "app/Components/ArtworkFilter/Filters/CategoriesOptions"
import { ColorsOptionsScreen } from "app/Components/ArtworkFilter/Filters/ColorsOptions"
import { EstimateRangeOptionsScreen } from "app/Components/ArtworkFilter/Filters/EstimateRangeOptions"
Expand Down Expand Up @@ -83,6 +84,7 @@ export type ArtworkFilterNavigationStack = {
ArtistSeriesOptionsScreen: undefined
AttributionClassOptionsScreen: undefined
AuctionHouseOptionsScreen: undefined
AvailabilityOptionsScreen: undefined
CategoriesOptionsScreen: undefined
ColorOptionsScreen: undefined
ColorsOptionsScreen: undefined
Expand Down Expand Up @@ -340,6 +342,7 @@ export const ArtworkFilterNavigator: React.FC<ArtworkFilterProps> = (props) => {
component={AttributionClassOptionsScreen}
/>
<Stack.Screen name="AuctionHouseOptionsScreen" component={AuctionHouseOptionsScreen} />
<Stack.Screen name="AvailabilityOptionsScreen" component={AvailabilityOptionsScreen} />
<Stack.Screen name="ColorsOptionsScreen" component={ColorsOptionsScreen} />
<Stack.Screen
name="EstimateRangeOptionsScreen"
Expand Down
12 changes: 11 additions & 1 deletion src/app/Components/ArtworkFilter/ArtworkFilterOptionsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const ArtworkFilterOptionsScreen: React.FC<
StackScreenProps<ArtworkFilterNavigationStack, "FilterOptionsScreen">
> = ({ navigation, route }) => {
const enableArtistSeriesFilter = useFeatureFlag("AREnableArtistSeriesFilter")
const enableAvailabilityFilter = useFeatureFlag("AREnableAvailabilityFilter")
const tracking = useTracking()
const { closeModal, id, mode, slug, title = "Sort & Filter" } = route.params

Expand Down Expand Up @@ -104,7 +105,9 @@ export const ArtworkFilterOptionsScreen: React.FC<
.filter((filterOption) => filterOption.filterType)
// Filter out the Artist Series filter if the feature flag is disabled
.filter(
(filterOption) => enableArtistSeriesFilter || filterOption.filterType !== "artistSeriesIDs"
(filterOption) =>
(enableArtistSeriesFilter || filterOption.filterType !== "artistSeriesIDs") &&
(enableAvailabilityFilter || filterOption.filterType !== "availability")
)

const clearAllFilters = () => {
Expand Down Expand Up @@ -215,6 +218,7 @@ export const getStaticFilterOptionsByMode = (
default:
return [
filterOptionToDisplayConfigMap.attributionClass,
filterOptionToDisplayConfigMap.availability,
filterOptionToDisplayConfigMap.sort,
filterOptionToDisplayConfigMap.waysToBuy,
]
Expand Down Expand Up @@ -414,6 +418,11 @@ export const filterOptionToDisplayConfigMap: Record<string, FilterDisplayConfig>
filterType: "estimateRange",
ScreenComponent: "EstimateRangeOptionsScreen",
},
availability: {
displayText: FilterDisplayName.availability,
filterType: "availability",
ScreenComponent: "AvailabilityOptionsScreen",
},
partnerIDs: {
displayText: FilterDisplayName.partnerIDs,
filterType: "partnerIDs",
Expand Down Expand Up @@ -507,6 +516,7 @@ const ArtistArtworksFiltersSorted: FilterScreen[] = [
"artistSeriesIDs",
"sizes",
"waysToBuy",
"availability",
"materialsTerms",
"locationCities",
"majorPeriods",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { fireEvent } from "@testing-library/react-native"
import { FilterParamName } from "app/Components/ArtworkFilter/ArtworkFilterHelpers"
import {
ArtworkFiltersState,
ArtworkFiltersStoreProvider,
getArtworkFiltersModel,
} from "app/Components/ArtworkFilter/ArtworkFilterStore"
import { MockFilterScreen } from "app/Components/ArtworkFilter/FilterTestHelper"
import { AvailabilityOptionsScreen } from "app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx"
import { __globalStoreTestUtils__ } from "app/store/GlobalStore"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"
import { getEssentialProps } from "./helper"

describe(AvailabilityOptionsScreen, () => {
beforeEach(() => {
__globalStoreTestUtils__?.injectFeatureFlags({ AREnableAvailabilityFilter: true })
})

const initialState: ArtworkFiltersState = {
aggregations: [],
appliedFilters: [],
applyFilters: false,
counts: {
total: null,
followedArtists: null,
},
showFilterArtworksModal: false,
sizeMetric: "cm",
filterType: "artwork",
previouslyAppliedFilters: [],
selectedFilters: [],
}

const MockAvailabilityOptionsScreen = ({
initialData = initialState,
}: {
initialData?: ArtworkFiltersState
}) => {
return (
<ArtworkFiltersStoreProvider
runtimeModel={{
...getArtworkFiltersModel(),
...initialData,
}}
>
<AvailabilityOptionsScreen {...getEssentialProps()} />
</ArtworkFiltersStoreProvider>
)
}

describe("no filters are selected", () => {
it("renders all options", () => {
const { getByText } = renderWithWrappers(
<MockAvailabilityOptionsScreen initialData={initialState} />
)

expect(getByText("Only works for sale")).toBeTruthy()
})
})

describe("a filter is selected", () => {
const state: ArtworkFiltersState = {
...initialState,
selectedFilters: [
{
displayText: "Only works for sale",
paramName: FilterParamName.forSale,
paramValue: true,
},
],
}

it("displays the number of the selected filters on the filter modal screen", () => {
const { getByText } = renderWithWrappers(<MockFilterScreen initialState={state} />)

expect(getByText("Availability • 1")).toBeTruthy()
})

it("toggles selected filters 'ON' and unselected filters 'OFF", async () => {
const { getAllByA11yState } = renderWithWrappers(
<MockAvailabilityOptionsScreen initialData={state} />
)

let options = getAllByA11yState({ checked: true })

expect(options).toHaveLength(1)
expect(options[0]).toHaveTextContent("Only works for sale")

fireEvent.press(options[0])

options = getAllByA11yState({ checked: false })

expect(options).toHaveLength(1)
expect(options[0]).toHaveTextContent("Only works for sale")
})
})
})
79 changes: 79 additions & 0 deletions src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { StackScreenProps } from "@react-navigation/stack"
import {
FilterData,
FilterDisplayName,
FilterParamName,
} from "app/Components/ArtworkFilter/ArtworkFilterHelpers"
import { ArtworkFilterNavigationStack } from "app/Components/ArtworkFilter/ArtworkFilterNavigator"
import {
ArtworksFiltersStore,
useSelectedOptionsDisplay,
} from "app/Components/ArtworkFilter/ArtworkFilterStore"
import React, { useState } from "react"
import { MultiSelectOptionScreen } from "./MultiSelectOption"

type AvailabilityOptionsScreenProps = StackScreenProps<
ArtworkFilterNavigationStack,
"AvailabilityOptionsScreen"
>

export const OPTIONS: FilterData[] = [
{
displayText: "Only works for sale",
paramName: FilterParamName.forSale,
},
]

export const AvailabilityOptionsScreen: React.FC<AvailabilityOptionsScreenProps> = ({
navigation,
}) => {
const selectFiltersAction = ArtworksFiltersStore.useStoreActions(
(state) => state.selectFiltersAction
)

const selectedOptions = useSelectedOptionsDisplay()
const options = OPTIONS.map((option) => {
const selectedOptionByParamName = selectedOptions.find(
(selectedOption) => selectedOption.paramName === option.paramName
)

return {
...option,
paramValue: selectedOptionByParamName?.paramValue || undefined,
}
})

const [key, setKey] = useState(0)

const handleSelect = (option: FilterData, updatedValue: boolean) => {
selectFiltersAction({
displayText: option.displayText,
paramValue: updatedValue || undefined,
paramName: option.paramName,
})
}

const handleClear = () => {
options.map((option) => {
selectFiltersAction({ ...option, paramValue: undefined })
})

// Force re-render
setKey((n) => n + 1)
}

const selected = options.filter((option) => option.paramValue)

return (
<MultiSelectOptionScreen
key={key}
onSelect={handleSelect}
filterHeaderText={FilterDisplayName.availability}
filterOptions={options}
navigation={navigation}
{...(selected.length > 0
? { rightButtonText: "Clear", onRightButtonPress: handleClear }
: {})}
/>
)
}
1 change: 1 addition & 0 deletions src/app/Components/ArtworkFilter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type FilterScreen =
| "artistSeriesIDs"
| "artistsIFollow"
| "attributionClass"
| "availability"
| "categories"
| "color"
| "colors"
Expand Down
6 changes: 6 additions & 0 deletions src/app/store/config/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ export const features = {
showInDevMenu: true,
echoFlagKey: "AREnableNewSearchModal",
},
AREnableAvailabilityFilter: {
description: "Enable availability filter",
readyForRelease: false,
showInDevMenu: true,
// echoFlagKey: "AREnableAvailabilityFilter",
},
} satisfies { [key: string]: FeatureDescriptor }

export interface DevToggleDescriptor {
Expand Down