Skip to content

Commit

Permalink
Make parents with themselves as only children not expandable (#376)
Browse files Browse the repository at this point in the history
Closes #311
  • Loading branch information
wbazant authored Jun 12, 2024
1 parent 090daae commit 2039c95
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 122 deletions.
24 changes: 10 additions & 14 deletions src/components/filter/Filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styled from 'styled-components/macro'

import { filtersChanged, selectionChanged } from '../../redux/filterSlice'
import { useTypesById } from '../../redux/useTypesById'
import { updateTreeCounts } from '../../utils/buildTypeSchema'
import { constructTypesTreeForSelection } from '../../utils/buildTypeSchema'
import Input from '../ui/Input'
import { CheckboxFilters } from './CheckboxFilters'
import FilterButtons from './FilterButtons'
Expand Down Expand Up @@ -86,25 +86,25 @@ const Filter = ({ isOpen }) => {
const { typesById } = useTypesById()
const filters = useSelector((state) => state.filter)
const {
types,
isLoading,
countsById,
treeData,
allTypes,
types,
childrenById,
showOnlyOnMap,
scientificNameById,
} = filters

const treeDataWithUpdatedCounts = useMemo(
const typesTreeForSelection = useMemo(
() =>
updateTreeCounts(
treeData,
constructTypesTreeForSelection(
allTypes,
countsById,
showOnlyOnMap,
childrenById,
scientificNameById,
),
[treeData, countsById, showOnlyOnMap, childrenById, scientificNameById],
[allTypes, countsById, showOnlyOnMap, childrenById, scientificNameById],
)

useLayoutEffect(() => {
Expand Down Expand Up @@ -154,16 +154,12 @@ const Filter = ({ isOpen }) => {
</TreeFiltersContainer>
{didMount.current ? (
<RCTreeSelect
data={treeDataWithUpdatedCounts}
data={typesTreeForSelection}
loading={isLoading}
onChange={(selectedTypes) =>
dispatch(
selectionChanged(
selectedTypes.filter((t) => !t.includes('root')),
),
)
dispatch(selectionChanged(selectedTypes))
}
checkedTypes={types}
types={types}
searchValue={searchValue}
/>
) : (
Expand Down
10 changes: 6 additions & 4 deletions src/components/filter/RCTreeSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TreeSelect from 'rc-tree-select'
import { useState } from 'react'
import styled from 'styled-components/macro'

import { RC_ROOT_ID } from '../../utils/buildTypeSchema'
import { ReactComponent as ArrowIcon } from './arrow.svg'

const TreeSelectContainer = styled.div`
Expand Down Expand Up @@ -109,7 +110,7 @@ const TreeSelectContainer = styled.div`
}
`

const RCTreeSelect = ({ data, onChange, checkedTypes, searchValue }) => {
const RCTreeSelect = ({ data, onChange, types, searchValue }) => {
// useState is necessary instead of useRef in order to restore the container ref whenever the tree re-renders
const [treeSelectContainerRef, setTreeSelectContainerRef] = useState(null)

Expand Down Expand Up @@ -140,12 +141,13 @@ const RCTreeSelect = ({ data, onChange, checkedTypes, searchValue }) => {
overflow: 'auto',
}}
treeData={data}
value={checkedTypes}
value={types}
treeCheckable
onChange={onChange}
treeDataSimpleMode={{
id: 'value',
rootPId: 'null',
id: 'rcId',
pId: 'rcParentId',
rootPId: RC_ROOT_ID,
}}
treeNodeFilterProp="searchValue"
open
Expand Down
20 changes: 4 additions & 16 deletions src/redux/filterSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

import { getTypeCounts } from '../utils/api'
import {
buildTypeSchema,
getChildrenById,
getScientificNameById,
getTypesWithPendingCategory,
PENDING_ID,
} from '../utils/buildTypeSchema'
import { fetchAllTypes } from './miscSlice'
import { selectParams } from './selectParams'
Expand Down Expand Up @@ -34,8 +31,8 @@ export const fetchFilterCounts = createAsyncThunk(
export const filterSlice = createSlice({
name: 'filter',
initialState: {
allTypes: [],
types: null,
treeData: [],
childrenById: {},
scientificNameById: {},
muni: true,
Expand Down Expand Up @@ -72,19 +69,10 @@ export const filterSlice = createSlice({
[updateSelection]: (state, action) => ({ ...state, ...action.payload }),

[fetchAllTypes.fulfilled]: (state, action) => {
const typesWithPendingCategory = getTypesWithPendingCategory([
...action.payload,
{
id: PENDING_ID,
parent_id: null,
name: 'Pending Review',
},
])
const childrenById = getChildrenById(typesWithPendingCategory)
state.childrenById = childrenById
state.treeData = buildTypeSchema(typesWithPendingCategory, childrenById)
state.scientificNameById = getScientificNameById(action.payload)
state.allTypes = action.payload
state.types = action.payload.map((t) => `${t.id}`)
state.childrenById = getChildrenById(action.payload)
state.scientificNameById = getScientificNameById(action.payload)
},
},
})
Expand Down
194 changes: 106 additions & 88 deletions src/utils/buildTypeSchema.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import TreeNodeText from '../components/filter/TreeNodeText'

const PENDING_ID = 'PENDING'
const RC_ROOT_ID = 'RC_ROOT'

const getChildrenById = (types) => {
const children = {}
types.forEach(({ id, parent_id }) => {
if (id !== parent_id) {
if (!children[parent_id]) {
children[parent_id] = [id]
types.forEach(({ id, parent_id, pending }) => {
const pendingOrParentId = pending ? PENDING_ID : parent_id
if (pendingOrParentId && id !== pendingOrParentId) {
if (!children[pendingOrParentId]) {
children[pendingOrParentId] = [id]
} else {
children[parent_id].push(id)
children[pendingOrParentId].push(id)
}
}
})
Expand Down Expand Up @@ -37,37 +39,6 @@ const getTotalCount = (counts, id, childrenById, countsById) => {
return counts[id]
}

const getTypesWithPendingCategory = (types) =>
types.map((t) => ({
...t,
parent_id: t.pending ? PENDING_ID : t.parent_id,
}))

const getTypesWithRootLabels = (types, childrenById) =>
types.map((t) => {
const { id, parent_id } = t
const isParent = childrenById[id]
return {
...t,
value: isParent ? `root-${id}` : `${id}`,
pId: parent_id ? `root-${parent_id}` : 'null',
}
})

const addOtherCategories = (types, childrenById) => {
const typesWithOtherCategory = [...types]
types.forEach((t) => {
if (childrenById[t.id] && t.id !== PENDING_ID) {
typesWithOtherCategory.push({
...t,
value: `${t.id}`,
pId: `root-${t.id}`,
})
}
})
return typesWithOtherCategory
}

const sortTypes = (types) =>
types
.filter((t) => t.scientific_names?.length > 0)
Expand All @@ -91,81 +62,128 @@ const getNames = (type) => {
}
}

// Builds and sorts the type tree on page load
const buildTypeSchema = (types, childrenById) => {
const typesWithRootLabels = getTypesWithRootLabels(types, childrenById)

const typesWithOtherCategory = addOtherCategories(
typesWithRootLabels,
childrenById,
)

const sortedTypes = sortTypes(typesWithOtherCategory)

return sortedTypes.map((type) => {
const { commonName, scientificName } = getNames(type)

return {
...type,
pId: type.pId,
value: type.value,
searchValue: scientificName
? `${commonName} ${scientificName}`
: `${commonName}`,
}
})
}

// Updates cumulative counts and node titles in the type tree
const updateTreeCounts = (
treeData,
/*
* Construct a tree corresponding to current selection on map
* - if showOnlyOnMap selected, only keep types with nonzero counts, which flattens the tree
* - add parent types that group multiple types belonging together where necessary
* - add id, parent_id, title props
*
* Special cases:
* - some types serve as grouping but also correspond to markers annotated to higher level
* - 'pending review' nodes always have a Pending Review parent
* - cultivar level types display with cultivar name only, if there is a parent species level type
*/
const constructTypesTreeForSelection = (
allTypes,
countsById,
showOnlyOnMap,
childrenById,
scientificNameById,
) => {
const totalCount = {}
treeData.forEach((t) => {
getTotalCount(totalCount, t.id, childrenById, countsById)
const totalCountsById = {}
allTypes.forEach((t) => {
getTotalCount(totalCountsById, t.id, childrenById, countsById)
})
getTotalCount(totalCountsById, PENDING_ID, childrenById, countsById)

const typeSchema = treeData.map((type) => {
const { commonName, scientificName } = getNames(type)
const parentScientificName = scientificNameById[type.parent_id]
const cultivarIndex =
scientificName?.startsWith(`${parentScientificName} '`) &&
scientificName?.indexOf("'")
const count = type.value.includes('root')
? totalCount[type.id]
: countsById[type.id] ?? 0
const typesForSelection = showOnlyOnMap
? allTypes.filter((t) => totalCountsById[t.id])
: [...allTypes]

const allIdsForSelection = {}
typesForSelection.forEach((t) => {
allIdsForSelection[t.id] = 1
})

const idsOfParents = showOnlyOnMap
? getChildrenById(typesForSelection)
: childrenById

const typesAndParentsForSelection = []

typesForSelection.forEach((t) => {
const { id, parent_id, pending } = t
const pendingOrParentId = pending ? PENDING_ID : parent_id
const isParentInSelectionWithOwnValue = idsOfParents[id] && countsById[id]
const hasParentInSelectionOrPending =
allIdsForSelection[pendingOrParentId] || pending
if (isParentInSelectionWithOwnValue) {
// Allow the user to select just the less specifically annotated type
typesAndParentsForSelection.push({
...t,
count: countsById[id],
rcId: `extra-child-id-${id}`,
value: `${id}`,
rcParentId: `${id}`,
})
}
typesAndParentsForSelection.push({
...t,
count: totalCountsById[id],
rcId: `${id}`,
value: isParentInSelectionWithOwnValue
? // Use bogus value to avoid conflict warning
// the checkbox still affects the correct value, `${id}`,
// because it is the parent of child with that value
`extra-group-value-${id}`
: `${id}`,
// The parent is the "Pending Review" if applicable,
// or parent_id if we decided to display it,
// or else a special "null" pId that makes it show up at top level
rcParentId: hasParentInSelectionOrPending
? pendingOrParentId
: RC_ROOT_ID,
})
})
// Include the special "Pending Review" parent if needed
if (typesForSelection.some((t) => t.pending)) {
typesAndParentsForSelection.push({
id: null,
parent_id: null,
value: PENDING_ID,
rcId: PENDING_ID,
rcParentId: RC_ROOT_ID,
name: 'Pending Review',
count: totalCountsById[PENDING_ID],
})
}
return sortTypes(typesAndParentsForSelection).map((type) => {
const { commonName, scientificName } = getNames(type)
const parentScientificName = scientificNameById[type.rcParentId]
const cultivarIndex = scientificName?.startsWith(
`${parentScientificName} '`,
)
? scientificName?.indexOf("'")
: -1
const isCultivarWithParentInSelection =
cultivarIndex !== -1 && type.rcParentId !== RC_ROOT_ID
return {
...type,
searchValue: scientificName
? `${commonName} ${scientificName}`
: `${commonName}`,
title: (
<TreeNodeText
commonName={commonName}
shouldIncludeScientificName={scientificName}
shouldIncludeCommonName={commonName && !cultivarIndex}
shouldIncludeCommonName={
commonName && !isCultivarWithParentInSelection
}
scientificName={
cultivarIndex === -1
? scientificName
: scientificName?.substring(cultivarIndex)
isCultivarWithParentInSelection
? scientificName?.substring(cultivarIndex)
: scientificName
}
count={count}
count={type.count}
/>
),
count,
}
})

return showOnlyOnMap ? typeSchema.filter((t) => t.count > 0) : typeSchema
}

export {
buildTypeSchema,
constructTypesTreeForSelection,
getChildrenById,
getScientificNameById,
getTypesWithPendingCategory,
PENDING_ID,
updateTreeCounts,
RC_ROOT_ID,
}

0 comments on commit 2039c95

Please sign in to comment.