Skip to content

Commit

Permalink
Limit select/deselect all to displayed and filtered types (#564)
Browse files Browse the repository at this point in the history
  • Loading branch information
wbazant authored Nov 9, 2024
1 parent 386547a commit 565cefd
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 96 deletions.
32 changes: 0 additions & 32 deletions src/components/filter/CheckboxFilters.js

This file was deleted.

87 changes: 50 additions & 37 deletions src/components/filter/Filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components/macro'

import { filtersChanged, selectionChanged } from '../../redux/viewChange'
import {
invasiveChanged,
muniChanged,
selectionChanged,
} from '../../redux/viewChange'
import buildSelectTree from '../../utils/buildSelectTree'
import Input from '../ui/Input'
import { CheckboxFilters } from './CheckboxFilters'
import FilterButtons from './FilterButtons'
import LabeledCheckbox from './LabeledCheckbox'
import RCTreeSelectSkeleton from './RCTreeSelectSkeleton'
import TreeSelect from './TreeSelect'

Expand Down Expand Up @@ -46,14 +51,29 @@ const MuniAndInvasiveCheckboxFilters = styled.div`

const Filter = () => {
const [searchValue, setSearchValue] = useState('')
const [showOnlyOnMap, setShowOnlyOnMap] = useState(true)
const setSearchValueDebounced = useMemo(
() => debounce(setSearchValue, 200),
[setSearchValue],
)

const dispatch = useDispatch()
const filters = useSelector((state) => state.filter)
const { countsById, types, muni, invasive } = useSelector(
(state) => state.filter,
)

const { typesAccess } = useSelector((state) => state.type)
const { tree: selectTree, visibleTypeIds } = useMemo(
() =>
buildSelectTree(
typesAccess,
countsById,
showOnlyOnMap,
searchValue,
types,
),
[typesAccess, countsById, showOnlyOnMap, searchValue, types],
)

const { t } = useTranslation()
return (
Expand All @@ -65,57 +85,50 @@ const Filter = () => {
placeholder={t('type')}
/>
<TreeFiltersContainer>
<CheckboxFilters
values={filters}
fields={[
{
field: 'showOnlyOnMap',
label: t('only_on_map'),
},
]}
onChange={(values) => {
dispatch(filtersChanged(values))
}}
<LabeledCheckbox
field="showOnlyOnMap"
value={showOnlyOnMap}
label={t('only_on_map')}
onChange={setShowOnlyOnMap}
style={{ display: 'inline-block', marginRight: '5px' }}
/>
<FilterButtons
onSelectAllClick={() =>
dispatch(
selectionChanged(
typesAccess.selectableTypes().map((type) => type.id),
),
onSelectAllClick={() => {
const newSelection = [...new Set([...types, ...visibleTypeIds])]
dispatch(selectionChanged(newSelection))
}}
onDeselectAllClick={() => {
const remainingSelection = types.filter(
(typeId) => !visibleTypeIds.some((t) => t === typeId),
)
}
onDeselectAllClick={() => dispatch(selectionChanged([]))}
dispatch(selectionChanged(remainingSelection))
}}
/>
</TreeFiltersContainer>
{typesAccess.isEmpty ? (
<RCTreeSelectSkeleton />
) : (
<TreeSelect
types={types}
onChange={(selectedTypes) =>
dispatch(selectionChanged(selectedTypes))
}
searchValue={searchValue}
selectTree={selectTree}
/>
)}
</div>
<MuniAndInvasiveCheckboxFilters>
<CheckboxFilters
values={filters}
fields={[
{
field: 'muni',
label: t('glossary.tree_inventory', { count: 2 }),
},
{
field: 'invasive',
label: t('invasives'),
},
]}
onChange={(values) => {
dispatch(filtersChanged(values))
}}
<LabeledCheckbox
field="muni"
value={muni}
label={t('glossary.tree_inventory', { count: 2 })}
onChange={(checked) => dispatch(muniChanged(checked))}
/>
<LabeledCheckbox
field="invasive"
value={invasive}
label={t('invasives')}
onChange={(checked) => dispatch(invasiveChanged(checked))}
/>
</MuniAndInvasiveCheckboxFilters>
</>
Expand Down
26 changes: 26 additions & 0 deletions src/components/filter/LabeledCheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from 'styled-components/macro'

import Checkbox from '../ui/Checkbox'

const StyledLabel = styled.label`
cursor: pointer;
display: flex;
align-items: center;
font-size: 0.875rem;
font-weight: bold;
color: ${({ theme }) => theme.secondaryText};
`

const LabeledCheckbox = ({ field, value, onChange, label, style }) => (
<StyledLabel htmlFor={field} style={style}>
<Checkbox
id={field}
checked={value}
name={field}
onChange={(event) => onChange(event.target.checked)}
/>
{label}
</StyledLabel>
)

export default LabeledCheckbox
25 changes: 3 additions & 22 deletions src/components/filter/TreeSelect.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import React, { useCallback, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import React, { useCallback, useState } from 'react'

import buildSelectTree from '../../utils/buildSelectTree'
import TreeSelectView from './TreeSelectView'

const TreeSelect = ({ onChange, searchValue }) => {
const TreeSelect = ({ types, onChange, selectTree }) => {
const [expandedNodes, setExpandedNodes] = useState(new Set())

const { types, showOnlyOnMap, countsById } = useSelector(
(state) => state.filter,
)
const { typesAccess } = useSelector((state) => state.type)

const renderTree = useMemo(
() =>
buildSelectTree(
typesAccess,
countsById,
showOnlyOnMap,
searchValue,
types,
),
[typesAccess, countsById, showOnlyOnMap, searchValue, types],
)

const handleToggle = (nodeId) => {
setExpandedNodes((prev) => {
const newSet = new Set(prev)
Expand Down Expand Up @@ -86,7 +67,7 @@ const TreeSelect = ({ onChange, searchValue }) => {

return (
<TreeSelectView
renderTree={renderTree}
renderTree={selectTree}
expandedNodes={expandedNodes}
handleToggle={handleToggle}
handleCheckboxChange={handleCheckboxChange}
Expand Down
1 change: 0 additions & 1 deletion src/redux/filterSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const filterSlice = createSlice({
invasive: false,
isLoading: false,
countsById: {},
showOnlyOnMap: true,
},
reducers: {
openFilter: (state) => {
Expand Down
10 changes: 8 additions & 2 deletions src/redux/viewChange.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ export const fetchLocations = () => (dispatch, getState) => {
}
}

export const filtersChanged = (filters) => (dispatch) => {
dispatch(updateSelection(filters))
export const invasiveChanged = (checked) => (dispatch) => {
dispatch(updateSelection({ invasive: checked }))
dispatch(fetchFilterCounts())
dispatch(fetchLocations())
}

export const muniChanged = (checked) => (dispatch) => {
dispatch(updateSelection({ muni: checked }))
dispatch(fetchFilterCounts())
dispatch(fetchLocations())
}
Expand Down
21 changes: 19 additions & 2 deletions src/utils/buildSelectTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SelectTreeBuilder {
private showOnlyOnMap: boolean
private searchValue: string
private selectedTypes: number[]
private visibleTypeIds: Set<number>

constructor(
typesAccess: TypesAccess,
Expand All @@ -33,6 +34,7 @@ class SelectTreeBuilder {
this.showOnlyOnMap = showOnlyOnMap
this.searchValue = searchValue.toLowerCase()
this.selectedTypes = selectedTypes
this.visibleTypeIds = new Set()
}

private isCultivarWithParentInSelection(
Expand Down Expand Up @@ -100,6 +102,10 @@ class SelectTreeBuilder {
return null
}

if (!node.isDisabled) {
this.visibleTypeIds.add(type.id)
}

const isCultivar = this.isCultivarWithParentInSelection(
type,
parent?.id ?? null,
Expand Down Expand Up @@ -156,6 +162,15 @@ class SelectTreeBuilder {
}
return count
}

getVisibleTypes(): number[] {
return Array.from(this.visibleTypeIds)
}
}

interface SelectTreeResult {
tree: RenderTreeNode[]
visibleTypeIds: number[]
}

function buildSelectTree(
Expand All @@ -164,15 +179,17 @@ function buildSelectTree(
showOnlyOnMap: boolean,
searchValue: string,
selectedTypes: number[],
): RenderTreeNode[] {
): SelectTreeResult {
const builder = new SelectTreeBuilder(
typesAccess,
countsById,
showOnlyOnMap,
searchValue,
selectedTypes,
)
return builder.buildRenderTree()
const tree = builder.buildRenderTree()
const visibleTypeIds = builder.getVisibleTypes()
return { tree, visibleTypeIds }
}

export default buildSelectTree

0 comments on commit 565cefd

Please sign in to comment.