diff --git a/webapp/src/components/Vendor/decentraland/NFTFilters/NFTFilters.types.ts b/webapp/src/components/Vendor/decentraland/NFTFilters/NFTFilters.types.ts
index 5eb2617d90..aeff1569fd 100644
--- a/webapp/src/components/Vendor/decentraland/NFTFilters/NFTFilters.types.ts
+++ b/webapp/src/components/Vendor/decentraland/NFTFilters/NFTFilters.types.ts
@@ -1,6 +1,11 @@
+import { Dispatch } from 'redux'
import { Network, Rarity } from '@dcl/schemas'
import { SortBy } from '../../../../modules/routing/types'
-import { browse } from '../../../../modules/routing/actions'
+import {
+ browse,
+ clearFilters,
+ ClearFiltersAction
+} from '../../../../modules/routing/actions'
import { WearableGender } from '../../../../modules/nft/wearable/types'
import { AssetType } from '../../../../modules/asset/types'
@@ -16,7 +21,9 @@ export type Props = {
wearableGenders: WearableGender[]
contracts: string[]
network?: Network
+ hasFiltersEnabled: boolean
onBrowse: typeof browse
+ onClearFilters: typeof clearFilters
}
export type MapStateProps = Pick<
@@ -32,6 +39,8 @@ export type MapStateProps = Pick<
| 'wearableGenders'
| 'contracts'
| 'network'
+ | 'hasFiltersEnabled'
>
-export type MapDispatchProps = {}
+export type MapDispatchProps = Pick
+export type MapDispatch = Dispatch
export type OwnProps = Pick
diff --git a/webapp/src/lib/utils.ts b/webapp/src/lib/utils.ts
new file mode 100644
index 0000000000..d7abcf3210
--- /dev/null
+++ b/webapp/src/lib/utils.ts
@@ -0,0 +1,32 @@
+/**
+ * Return a copy of the object, filtered to omit the blacklisted array of valid keys
+ * @param obj
+ * @param keys
+ */
+export function omit(
+ obj: Record,
+ keys: string[]
+): Record {
+ const newKeys = Object.keys(obj).filter(key => !keys.includes(key))
+ return pick(obj, newKeys)
+}
+
+/**
+ * Return a copy of the object, filtered to only have values for the whitelisted array of valid keys
+ * @param obj
+ * @param keys
+ */
+export function pick(
+ obj: Record,
+ keys: string[]
+): Record {
+ const result = {} as Record
+
+ for (const key of keys) {
+ if (obj.hasOwnProperty(key)) {
+ result[key] = obj[key]
+ }
+ }
+
+ return result
+}
diff --git a/webapp/src/modules/routing/actions.ts b/webapp/src/modules/routing/actions.ts
index cb161f2879..f8455bff2b 100644
--- a/webapp/src/modules/routing/actions.ts
+++ b/webapp/src/modules/routing/actions.ts
@@ -27,3 +27,10 @@ export const setIsLoadMore = (isLoadMore: boolean) =>
action(SET_IS_LOAD_MORE, { isLoadMore })
export type SetIsLoadMoreAction = ReturnType
+
+// Clear filters
+export const CLEAR_FILTERS = 'Clear filters'
+
+export const clearFilters = () => action(CLEAR_FILTERS)
+
+export type ClearFiltersAction = ReturnType
diff --git a/webapp/src/modules/routing/sagas.spec.ts b/webapp/src/modules/routing/sagas.spec.ts
new file mode 100644
index 0000000000..a688a455da
--- /dev/null
+++ b/webapp/src/modules/routing/sagas.spec.ts
@@ -0,0 +1,60 @@
+import { Network, Rarity } from '@dcl/schemas'
+import { getLocation, push } from 'connected-react-router'
+import { expectSaga } from 'redux-saga-test-plan'
+import { call, select } from 'redux-saga/effects'
+import { AssetType } from '../asset/types'
+import { WearableGender } from '../nft/wearable/types'
+import { View } from '../ui/types'
+import { VendorName } from '../vendor'
+import { clearFilters } from './actions'
+import {
+ buildBrowseURL,
+ fetchAssetsFromRoute,
+ getCurrentBrowseOptions,
+ routingSaga
+} from './sagas'
+import { BrowseOptions, SortBy } from './types'
+
+describe('when handling the clear filters request action', () => {
+ it("should fetch assets and change the URL by clearing the filter's browse options and restarting the page counter", () => {
+ const browseOptions: BrowseOptions = {
+ assetType: AssetType.ITEM,
+ address: '0x...',
+ vendor: VendorName.DECENTRALAND,
+ section: 'aSection',
+ page: 1,
+ view: View.MARKET,
+ sortBy: SortBy.NAME,
+ search: 'aText',
+ onlyOnSale: true,
+ isMap: false,
+ isFullscreen: false,
+ wearableRarities: [Rarity.EPIC],
+ wearableGenders: [WearableGender.FEMALE],
+ contracts: ['aContract'],
+ network: Network.ETHEREUM
+ }
+
+ const browseOptionsWithoutFilters: BrowseOptions = { ...browseOptions }
+ delete browseOptionsWithoutFilters.wearableRarities
+ delete browseOptionsWithoutFilters.wearableGenders
+ delete browseOptionsWithoutFilters.network
+ delete browseOptionsWithoutFilters.contracts
+ delete browseOptionsWithoutFilters.page
+
+ const pathname = 'aPath'
+
+ return expectSaga(routingSaga)
+ .provide([
+ [call(getCurrentBrowseOptions), browseOptions],
+ [select(getLocation), { pathname }],
+ [
+ call(fetchAssetsFromRoute, browseOptionsWithoutFilters),
+ Promise.resolve()
+ ]
+ ])
+ .put(push(buildBrowseURL(pathname, browseOptionsWithoutFilters)))
+ .dispatch(clearFilters())
+ .run({ silenceTimeout: true })
+ })
+})
diff --git a/webapp/src/modules/routing/sagas.ts b/webapp/src/modules/routing/sagas.ts
index 4e7f6170e6..0991b8d750 100644
--- a/webapp/src/modules/routing/sagas.ts
+++ b/webapp/src/modules/routing/sagas.ts
@@ -1,6 +1,9 @@
-import { takeEvery, put, select } from 'redux-saga/effects'
+import { takeEvery, put, select, call } from 'redux-saga/effects'
import { push, getLocation } from 'connected-react-router'
import { NFTCategory } from '@dcl/schemas'
+import { omit } from '../../lib/utils'
+import { AssetType } from '../asset/types'
+import { fetchItemsRequest } from '../item/actions'
import { VendorName } from '../vendor/types'
import { View } from '../ui/types'
import { getView } from '../ui/browse/selectors'
@@ -41,15 +44,15 @@ import {
BrowseAction,
FETCH_ASSETS_FROM_ROUTE,
FetchAssetsFromRouteAction,
- setIsLoadMore
+ setIsLoadMore,
+ CLEAR_FILTERS
} from './actions'
import { BrowseOptions, Section } from './types'
-import { AssetType } from '../asset/types'
-import { fetchItemsRequest } from '../item/actions'
export function* routingSaga() {
yield takeEvery(FETCH_ASSETS_FROM_ROUTE, handleFetchAssetsFromRoute)
yield takeEvery(BROWSE, handleBrowse)
+ yield takeEvery(CLEAR_FILTERS, handleClearFilters)
}
function* handleFetchAssetsFromRoute(action: FetchAssetsFromRouteAction) {
@@ -59,21 +62,44 @@ function* handleFetchAssetsFromRoute(action: FetchAssetsFromRouteAction) {
yield fetchAssetsFromRoute(newOptions)
}
+function* handleClearFilters() {
+ const browseOptions: BrowseOptions = yield call(getCurrentBrowseOptions)
+ const { pathname }: ReturnType = yield select(getLocation)
+
+ const clearedBrowseOptions: BrowseOptions = omit(browseOptions, [
+ 'wearableRarities',
+ 'wearableGenders',
+ 'network',
+ 'contracts',
+ 'page'
+ ])
+
+ yield call(fetchAssetsFromRoute, clearedBrowseOptions)
+ yield put(push(buildBrowseURL(pathname, clearedBrowseOptions)))
+}
+
function* handleBrowse(action: BrowseAction) {
const options: BrowseOptions = yield getNewBrowseOptions(
action.payload.options
)
- yield fetchAssetsFromRoute(options)
-
const { pathname }: ReturnType = yield select(getLocation)
- const params = getSearchParams(options)
- yield put(push(params ? `${pathname}?${params.toString()}` : pathname))
+
+ yield fetchAssetsFromRoute(options)
+ yield put(push(buildBrowseURL(pathname, options)))
}
// ------------------------------------------------
// Utility functions, not handlers
-function* fetchAssetsFromRoute(options: BrowseOptions) {
+export function buildBrowseURL(
+ pathname: string,
+ browseOptions: BrowseOptions
+): string {
+ const params = getSearchParams(browseOptions)
+ return params ? `${pathname}?${params.toString()}` : pathname
+}
+
+export function* fetchAssetsFromRoute(options: BrowseOptions) {
const isItems = options.assetType === AssetType.ITEM
const view = options.view!
const vendor = options.vendor!
@@ -151,10 +177,12 @@ function* fetchAssetsFromRoute(options: BrowseOptions) {
}
}
-function* getNewBrowseOptions(
- current: BrowseOptions
-): Generator {
- let previous: BrowseOptions = {
+export function* getCurrentBrowseOptions(): Generator<
+ unknown,
+ BrowseOptions,
+ unknown
+> {
+ return {
assetType: yield select(getAssetType),
address: yield getAddress(),
vendor: yield select(getVendor),
@@ -170,7 +198,13 @@ function* getNewBrowseOptions(
wearableGenders: yield select(getWearableGenders),
contracts: yield select(getContracts),
network: yield select(getNetwork)
- }
+ } as BrowseOptions
+}
+
+function* getNewBrowseOptions(
+ current: BrowseOptions
+): Generator {
+ let previous = yield getCurrentBrowseOptions()
current = yield deriveCurrentOptions(previous, current)
const view = deriveView(previous, current)
const vendor = deriveVendor(previous, current)
diff --git a/webapp/src/modules/routing/selectors.spec.ts b/webapp/src/modules/routing/selectors.spec.ts
new file mode 100644
index 0000000000..df864e1e35
--- /dev/null
+++ b/webapp/src/modules/routing/selectors.spec.ts
@@ -0,0 +1,41 @@
+import { Rarity } from '@dcl/schemas'
+import { WearableGender } from '../nft/wearable/types'
+import { hasFiltersEnabled } from './selectors'
+
+describe('when getting if the are filters set', () => {
+ describe('when the network filter is set', () => {
+ it('should return true', () => {
+ expect(hasFiltersEnabled.resultFunc('aNetwork', [], [], [])).toBe(true)
+ })
+ })
+
+ describe('when the genders filter is set', () => {
+ it('should return true', () => {
+ expect(
+ hasFiltersEnabled.resultFunc(undefined, [WearableGender.FEMALE], [], [])
+ ).toBe(true)
+ })
+ })
+
+ describe('when the rarities filter is set', () => {
+ it('should return true', () => {
+ expect(
+ hasFiltersEnabled.resultFunc(undefined, [], [Rarity.COMMON], [])
+ ).toBe(true)
+ })
+ })
+
+ describe('when the contracts filter is set', () => {
+ it('should return true', () => {
+ expect(hasFiltersEnabled.resultFunc(undefined, [], [], ['0x.....'])).toBe(
+ true
+ )
+ })
+ })
+
+ describe('when the network, the genders, the rarities and the contracts filters is not set', () => {
+ it('should return false', () => {
+ expect(hasFiltersEnabled.resultFunc(undefined, [], [], [])).toBe(false)
+ })
+ })
+})
diff --git a/webapp/src/modules/routing/selectors.ts b/webapp/src/modules/routing/selectors.ts
index e0a2b777a2..6837d0f04e 100644
--- a/webapp/src/modules/routing/selectors.ts
+++ b/webapp/src/modules/routing/selectors.ts
@@ -193,3 +193,29 @@ export const getAssetType = createSelector<
}
return results
})
+
+export const hasFiltersEnabled = createSelector<
+ RootState,
+ string | undefined,
+ WearableGender[],
+ Rarity[],
+ string[],
+ boolean
+>(
+ getNetwork,
+ getWearableGenders,
+ getWearableRarities,
+ getContracts,
+ (network, genders, rarities, contracts) => {
+ const hasNetworkFilter = network !== undefined
+ const hasGenderFilter = genders.length > 0
+ const hasRarityFilter = rarities.length > 0
+ const hasContractsFilter = contracts.length > 0
+ return (
+ hasNetworkFilter ||
+ hasGenderFilter ||
+ hasRarityFilter ||
+ hasContractsFilter
+ )
+ }
+)
diff --git a/webapp/src/modules/translation/locales/en.json b/webapp/src/modules/translation/locales/en.json
index 86e69c6e41..774ad20c4d 100644
--- a/webapp/src/modules/translation/locales/en.json
+++ b/webapp/src/modules/translation/locales/en.json
@@ -80,7 +80,8 @@
"name": "Name",
"newest": "Newest",
"recently_listed": "Recently listed",
- "cheapest": "Cheapest"
+ "cheapest": "Cheapest",
+ "clear": "Clear filters"
},
"home_page": {
"title": "Decentraland Marketplace",
diff --git a/webapp/src/modules/translation/locales/es.json b/webapp/src/modules/translation/locales/es.json
index 7ee83cd7bf..f2cb8dc903 100644
--- a/webapp/src/modules/translation/locales/es.json
+++ b/webapp/src/modules/translation/locales/es.json
@@ -78,7 +78,8 @@
"name": "Nombre",
"newest": "Más nuevos",
"recently_listed": "Listados recientemente",
- "cheapest": "Más baratos"
+ "cheapest": "Más baratos",
+ "clear": "Borrar filtros"
},
"home_page": {
"title": "Mercado de Decentraland",
diff --git a/webapp/src/modules/translation/locales/zh.json b/webapp/src/modules/translation/locales/zh.json
index 770ac10620..c98d652888 100644
--- a/webapp/src/modules/translation/locales/zh.json
+++ b/webapp/src/modules/translation/locales/zh.json
@@ -78,7 +78,8 @@
"name": "名称",
"newest": "最新",
"recently_listed": "最新上市",
- "cheapest": "最低价"
+ "cheapest": "最低价",
+ "clear": "清除过滤器"
},
"home_page": {
"title": "Decentraland Marketplace",