Skip to content

Commit

Permalink
feat: Add clear all filters (decentraland#412)
Browse files Browse the repository at this point in the history
* feat: Add clear filters

* chore: Add tests

* Remove important

* chore: Use omit from decentraland-commons

* fix: Replace SSH for HTTPS in dep

* fix: Remove decentraland-commons
  • Loading branch information
LautaroPetaccio authored Aug 25, 2021
1 parent d41e03b commit 78283e8
Show file tree
Hide file tree
Showing 14 changed files with 28,679 additions and 49 deletions.
28,421 changes: 28,396 additions & 25 deletions webapp/package-lock.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions webapp/src/components/Vendor/NFTFilters/NFTFilters.css
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@
border-radius: 0 4px 4px 0;
}

.NFTFilters .clear-filters {
font-size: 12px;
line-height: 12px;
text-transform: uppercase;
cursor: pointer;
color: var(--primary);
}

/*------------------
FiltersModal
*/
Expand Down Expand Up @@ -204,6 +212,11 @@
color: var(--text);
}

.FiltersModal .clear-filters-modal > span {
text-transform: uppercase;
color: var(--text);
}

@media (max-width: 768px) {
.NFTFilters .topbar {
align-items: normal;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { connect } from 'react-redux'

import { RootState } from '../../../../modules/reducer'
import { clearFilters } from '../../../../modules/routing/actions'
import { hasFiltersEnabled } from '../../../../modules/routing/selectors'
import { getCount } from '../../../../modules/ui/browse/selectors'
import {
getSection,
Expand All @@ -18,7 +20,8 @@ import {
MapStateProps,
MapDispatchProps,
OwnProps,
Props
Props,
MapDispatch
} from './NFTFilters.types'
import NFTFilters from './NFTFilters'

Expand All @@ -33,10 +36,13 @@ const mapState = (state: RootState): MapStateProps => ({
wearableRarities: getWearableRarities(state),
wearableGenders: getWearableGenders(state),
contracts: getContracts(state),
network: getNetwork(state)
network: getNetwork(state),
hasFiltersEnabled: hasFiltersEnabled(state)
})

const mapDispatch = () => ({})
const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
onClearFilters: () => dispatch(clearFilters())
})

const mergeProps = (
stateProps: MapStateProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
Dropdown,
DropdownProps,
Responsive,
Modal
Modal,
Icon,
NotMobile
} from 'decentraland-ui'
import { Network, NFTCategory, Rarity } from '@dcl/schemas'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
Expand Down Expand Up @@ -36,7 +38,9 @@ const NFTFilters = (props: Props) => {
contracts,
network,
onBrowse,
assetType
assetType,
hasFiltersEnabled,
onClearFilters
} = props

const category = section ? getCategoryFromSection(section) : undefined
Expand Down Expand Up @@ -197,6 +201,18 @@ const NFTFilters = (props: Props) => {
placeholder={searchPlaceholder}
onChange={handleSearch}
/>
<NotMobile>
{hasFiltersEnabled && (
<div className="clear-filters" onClick={onClearFilters}>
<Icon
aria-label="Clear filters"
aria-hidden="false"
name="close"
/>
<span>{t('filters.clear')}</span>
</div>
)}
</NotMobile>
<Responsive
minWidth={Responsive.onlyTablet.minWidth}
className="topbar-filter"
Expand Down Expand Up @@ -284,6 +300,18 @@ const NFTFilters = (props: Props) => {
>
<Modal.Header>{t('nft_filters.filter')}</Modal.Header>
<Modal.Content>
{hasFiltersEnabled && (
<div className="filter-row">
<div className="clear-filters-modal" onClick={onClearFilters}>
<Icon
aria-label="Clear filters"
aria-hidden="false"
name="close"
/>
<span>{t('filters.clear')}</span>
</div>
</div>
)}
{category === NFTCategory.WEARABLE ? (
<>
<div className="filter-row">
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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<
Expand All @@ -32,6 +39,8 @@ export type MapStateProps = Pick<
| 'wearableGenders'
| 'contracts'
| 'network'
| 'hasFiltersEnabled'
>
export type MapDispatchProps = {}
export type MapDispatchProps = Pick<Props, 'onClearFilters'>
export type MapDispatch = Dispatch<ClearFiltersAction>
export type OwnProps = Pick<Props, 'onBrowse' | 'isMap'>
32 changes: 32 additions & 0 deletions webapp/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
keys: string[]
): Record<string, unknown> {
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<string, unknown>,
keys: string[]
): Record<string, unknown> {
const result = {} as Record<string, unknown>

for (const key of keys) {
if (obj.hasOwnProperty(key)) {
result[key] = obj[key]
}
}

return result
}
7 changes: 7 additions & 0 deletions webapp/src/modules/routing/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ export const setIsLoadMore = (isLoadMore: boolean) =>
action(SET_IS_LOAD_MORE, { isLoadMore })

export type SetIsLoadMoreAction = ReturnType<typeof setIsLoadMore>

// Clear filters
export const CLEAR_FILTERS = 'Clear filters'

export const clearFilters = () => action(CLEAR_FILTERS)

export type ClearFiltersAction = ReturnType<typeof clearFilters>
60 changes: 60 additions & 0 deletions webapp/src/modules/routing/sagas.spec.ts
Original file line number Diff line number Diff line change
@@ -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 })
})
})
62 changes: 48 additions & 14 deletions webapp/src/modules/routing/sagas.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -59,21 +62,44 @@ function* handleFetchAssetsFromRoute(action: FetchAssetsFromRouteAction) {
yield fetchAssetsFromRoute(newOptions)
}

function* handleClearFilters() {
const browseOptions: BrowseOptions = yield call(getCurrentBrowseOptions)
const { pathname }: ReturnType<typeof getLocation> = 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<typeof getLocation> = 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!
Expand Down Expand Up @@ -151,10 +177,12 @@ function* fetchAssetsFromRoute(options: BrowseOptions) {
}
}

function* getNewBrowseOptions(
current: BrowseOptions
): Generator<unknown, BrowseOptions, any> {
let previous: BrowseOptions = {
export function* getCurrentBrowseOptions(): Generator<
unknown,
BrowseOptions,
unknown
> {
return {
assetType: yield select(getAssetType),
address: yield getAddress(),
vendor: yield select(getVendor),
Expand All @@ -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<unknown, BrowseOptions, any> {
let previous = yield getCurrentBrowseOptions()
current = yield deriveCurrentOptions(previous, current)
const view = deriveView(previous, current)
const vendor = deriveVendor(previous, current)
Expand Down
Loading

0 comments on commit 78283e8

Please sign in to comment.