From 5510c6489ac0dac7fbc48bc057d372d63530a793 Mon Sep 17 00:00:00 2001 From: Ole Date: Tue, 26 Nov 2024 17:37:10 +0100 Subject: [PATCH] feat: Add Availability filter to artwork filters (#11100) * feat: Add artworks availability filter * add filter --- .../ArtworkFilter/ArtworkFilterHelpers.ts | 15 ++- .../ArtworkFilter/ArtworkFilterNavigator.tsx | 3 + .../ArtworkFilterOptionsScreen.tsx | 12 ++- .../Filters/AvailabilityOptions.tests.tsx | 97 +++++++++++++++++++ .../Filters/AvailabilityOptions.tsx | 79 +++++++++++++++ src/app/Components/ArtworkFilter/types.ts | 1 + src/app/store/config/features.ts | 6 ++ 7 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tests.tsx create mode 100644 src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx diff --git a/src/app/Components/ArtworkFilter/ArtworkFilterHelpers.ts b/src/app/Components/ArtworkFilter/ArtworkFilterHelpers.ts index 57caa341597..b117c4e33f9 100644 --- a/src/app/Components/ArtworkFilter/ArtworkFilterHelpers.ts +++ b/src/app/Components/ArtworkFilter/ArtworkFilterHelpers.ts @@ -19,6 +19,7 @@ export enum FilterDisplayName { artistsIFollow = "Artist", artistSeriesIDs = "Artist Series", attributionClass = "Rarity", + availability = "Availability", categories = "Medium", colors = "Color", estimateRange = "Price/Estimate Range", @@ -50,6 +51,7 @@ export enum FilterParamName { colors = "colors", earliestCreatedYear = "earliestCreatedYear", estimateRange = "estimateRange", + forSale = "forSale", height = "height", keyword = "keyword", latestCreatedYear = "latestCreatedYear", @@ -87,6 +89,7 @@ export const QueryParamsToFilterValueMapping: Record = 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, @@ -140,6 +143,7 @@ export const ParamDefaultValues = { colors: [], earliestCreatedYear: undefined, estimateRange: "", + forSale: undefined, height: "*-*", includeArtworksByFollowedArtists: false, inquireableOnly: false, @@ -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, @@ -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< @@ -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, @@ -413,7 +420,7 @@ export const aggregationNameFromFilter: Record { const aggregationName = aggregationNameFromFilter[filterKey] - const aggregation = aggregations!.find((value) => value.slice === aggregationName) + const aggregation = aggregations.find((value) => value.slice === aggregationName) return aggregation } @@ -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 diff --git a/src/app/Components/ArtworkFilter/ArtworkFilterNavigator.tsx b/src/app/Components/ArtworkFilter/ArtworkFilterNavigator.tsx index 870d481fc94..9cc012d5acc 100644 --- a/src/app/Components/ArtworkFilter/ArtworkFilterNavigator.tsx +++ b/src/app/Components/ArtworkFilter/ArtworkFilterNavigator.tsx @@ -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" @@ -83,6 +84,7 @@ export type ArtworkFilterNavigationStack = { ArtistSeriesOptionsScreen: undefined AttributionClassOptionsScreen: undefined AuctionHouseOptionsScreen: undefined + AvailabilityOptionsScreen: undefined CategoriesOptionsScreen: undefined ColorOptionsScreen: undefined ColorsOptionsScreen: undefined @@ -340,6 +342,7 @@ export const ArtworkFilterNavigator: React.FC = (props) => { component={AttributionClassOptionsScreen} /> + > = ({ navigation, route }) => { const enableArtistSeriesFilter = useFeatureFlag("AREnableArtistSeriesFilter") + const enableAvailabilityFilter = useFeatureFlag("AREnableAvailabilityFilter") const tracking = useTracking() const { closeModal, id, mode, slug, title = "Sort & Filter" } = route.params @@ -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 = () => { @@ -215,6 +218,7 @@ export const getStaticFilterOptionsByMode = ( default: return [ filterOptionToDisplayConfigMap.attributionClass, + filterOptionToDisplayConfigMap.availability, filterOptionToDisplayConfigMap.sort, filterOptionToDisplayConfigMap.waysToBuy, ] @@ -414,6 +418,11 @@ export const filterOptionToDisplayConfigMap: Record filterType: "estimateRange", ScreenComponent: "EstimateRangeOptionsScreen", }, + availability: { + displayText: FilterDisplayName.availability, + filterType: "availability", + ScreenComponent: "AvailabilityOptionsScreen", + }, partnerIDs: { displayText: FilterDisplayName.partnerIDs, filterType: "partnerIDs", @@ -507,6 +516,7 @@ const ArtistArtworksFiltersSorted: FilterScreen[] = [ "artistSeriesIDs", "sizes", "waysToBuy", + "availability", "materialsTerms", "locationCities", "majorPeriods", diff --git a/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tests.tsx b/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tests.tsx new file mode 100644 index 00000000000..3b50ae26218 --- /dev/null +++ b/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tests.tsx @@ -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 ( + + + + ) + } + + describe("no filters are selected", () => { + it("renders all options", () => { + const { getByText } = renderWithWrappers( + + ) + + 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() + + expect(getByText("Availability • 1")).toBeTruthy() + }) + + it("toggles selected filters 'ON' and unselected filters 'OFF", async () => { + const { getAllByA11yState } = renderWithWrappers( + + ) + + 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") + }) + }) +}) diff --git a/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx b/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx new file mode 100644 index 00000000000..42ce5ec4bcf --- /dev/null +++ b/src/app/Components/ArtworkFilter/Filters/AvailabilityOptions.tsx @@ -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 = ({ + 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 ( + 0 + ? { rightButtonText: "Clear", onRightButtonPress: handleClear } + : {})} + /> + ) +} diff --git a/src/app/Components/ArtworkFilter/types.ts b/src/app/Components/ArtworkFilter/types.ts index 82670350c6c..a2ff5c1185b 100644 --- a/src/app/Components/ArtworkFilter/types.ts +++ b/src/app/Components/ArtworkFilter/types.ts @@ -7,6 +7,7 @@ export type FilterScreen = | "artistSeriesIDs" | "artistsIFollow" | "attributionClass" + | "availability" | "categories" | "color" | "colors" diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index 6c2073de99a..1b79d5b45d2 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -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 {