From 3a54a4cd7dd7097139db5ae7d4f4483a5618061a Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Tue, 12 Nov 2024 17:55:05 +0000 Subject: [PATCH 01/35] Checking in before running aider: In fetchAndLocalizeTypes.fulfilled, use currently selected categories --- src/components/filter/Filter.js | 25 +++++++++++++++++++++++++ src/redux/filterSlice.js | 12 +++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index c2fd3ea6f..7c8db0852 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components/macro' +import { categoryChanged } from '../../redux/filterSlice' import { invasiveChanged, muniChanged, @@ -118,6 +119,30 @@ const Filter = () => { )} + state.filter.categories.forager)} + label="Forager" + onChange={(checked) => dispatch(categoryChanged('forager', checked))} + /> + state.filter.categories.freegan)} + label="Freegan" + onChange={(checked) => dispatch(categoryChanged('freegan', checked))} + /> + state.filter.categories.grafter)} + label="Grafter" + onChange={(checked) => dispatch(categoryChanged('grafter', checked))} + /> + state.filter.categories.honeybee)} + label="Honeybee" + onChange={(checked) => dispatch(categoryChanged('honeybee', checked))} + /> { @@ -55,6 +61,10 @@ export const filterSlice = createSlice({ closeFilter: (state) => { state.isOpenInMobileLayout = false }, + categoryChanged: (state, action) => { + const [category, value] = action.payload + state.categories[category] = value + }, }, extraReducers: { [fetchFilterCounts.pending]: (state) => { @@ -86,6 +96,6 @@ export const filterSlice = createSlice({ }, }) -export const { openFilter, closeFilter } = filterSlice.actions +export const { openFilter, closeFilter, categoryChanged } = filterSlice.actions export default filterSlice.reducer From 00bba5b9ccde2ba51ae5c9fa6c38919c994bb920 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Tue, 12 Nov 2024 17:55:17 +0000 Subject: [PATCH 02/35] Aider prompt: In fetchAndLocalizeTypes.fulfilled, use currently selected categories --- src/redux/filterSlice.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/redux/filterSlice.js b/src/redux/filterSlice.js index aadeb4af1..d274abb13 100644 --- a/src/redux/filterSlice.js +++ b/src/redux/filterSlice.js @@ -89,8 +89,11 @@ export const filterSlice = createSlice({ [fetchAndLocalizeTypes.fulfilled]: (state, action) => { const typesAccess = action.payload + const selectedCategories = Object.entries(state.categories) + .filter(([_, isSelected]) => isSelected) + .map(([category]) => category) state.types = typesAccess - .selectableTypesWithCategories('forager', 'freegan') + .selectableTypesWithCategories(...selectedCategories) .map((t) => t.id) }, }, From 1c00c12fb14dadb6f7aa71264410ebadf0086f44 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:09:39 +0000 Subject: [PATCH 03/35] Aider prompt: Get categories in one useSelector on top --- src/components/filter/Filter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 7c8db0852..186430d5e 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -59,7 +59,7 @@ const Filter = () => { ) const dispatch = useDispatch() - const { countsById, types, muni, invasive } = useSelector( + const { countsById, types, muni, invasive, categories } = useSelector( (state) => state.filter, ) @@ -121,25 +121,25 @@ const Filter = () => { state.filter.categories.forager)} + value={categories.forager} label="Forager" onChange={(checked) => dispatch(categoryChanged('forager', checked))} /> state.filter.categories.freegan)} + value={categories.freegan} label="Freegan" onChange={(checked) => dispatch(categoryChanged('freegan', checked))} /> state.filter.categories.grafter)} + value={categories.grafter} label="Grafter" onChange={(checked) => dispatch(categoryChanged('grafter', checked))} /> state.filter.categories.honeybee)} + value={categories.honeybee} label="Honeybee" onChange={(checked) => dispatch(categoryChanged('honeybee', checked))} /> From 6a38d8a9f90ef8a595ec893e0a693477ec2dbb75 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:11:07 +0000 Subject: [PATCH 04/35] Checking in before running aider: Correctly pass args to categoryChanged --- src/components/filter/Filter.js | 1 + src/redux/filterSlice.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 186430d5e..a7a01e5fc 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -62,6 +62,7 @@ const Filter = () => { const { countsById, types, muni, invasive, categories } = useSelector( (state) => state.filter, ) + console.log(categories) const { typesAccess } = useSelector((state) => state.type) const { tree: selectTree, visibleTypeIds } = useMemo( diff --git a/src/redux/filterSlice.js b/src/redux/filterSlice.js index d274abb13..f59eddc52 100644 --- a/src/redux/filterSlice.js +++ b/src/redux/filterSlice.js @@ -62,7 +62,8 @@ export const filterSlice = createSlice({ state.isOpenInMobileLayout = false }, categoryChanged: (state, action) => { - const [category, value] = action.payload + console.log(action.payload) + const { category, value } = action.payload state.categories[category] = value }, }, From 537ad9bee62372982304c21282932535300a5522 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:11:21 +0000 Subject: [PATCH 05/35] Aider prompt: Correctly pass args to categoryChanged --- src/components/filter/Filter.js | 16 ++++++++++++---- src/redux/filterSlice.js | 1 - 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index a7a01e5fc..983e214a2 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -124,25 +124,33 @@ const Filter = () => { field="forager" value={categories.forager} label="Forager" - onChange={(checked) => dispatch(categoryChanged('forager', checked))} + onChange={(checked) => + dispatch(categoryChanged({ category: 'forager', value: checked })) + } /> dispatch(categoryChanged('freegan', checked))} + onChange={(checked) => + dispatch(categoryChanged({ category: 'freegan', value: checked })) + } /> dispatch(categoryChanged('grafter', checked))} + onChange={(checked) => + dispatch(categoryChanged({ category: 'grafter', value: checked })) + } /> dispatch(categoryChanged('honeybee', checked))} + onChange={(checked) => + dispatch(categoryChanged({ category: 'honeybee', value: checked })) + } /> { - console.log(action.payload) const { category, value } = action.payload state.categories[category] = value }, From 3c5c5a64d80827ecbbd81a14e5b22b33db009f1c Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:14:13 +0000 Subject: [PATCH 06/35] Checking in before running aider: Add feature to buildSelectTree: entries are only visible if at least one of their categories is checked. For disabled parents follow same logic as searchValue --- src/components/filter/Filter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 983e214a2..46b4b8ab2 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -62,7 +62,6 @@ const Filter = () => { const { countsById, types, muni, invasive, categories } = useSelector( (state) => state.filter, ) - console.log(categories) const { typesAccess } = useSelector((state) => state.type) const { tree: selectTree, visibleTypeIds } = useMemo( From 397b9a90e0516a97e1cb3d0f89eb256b6bd26e98 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:14:36 +0000 Subject: [PATCH 07/35] Aider prompt: Add feature to buildSelectTree: entries are only visible if at least one of their categories is checked. For disabled parents follow same logic as searchValue --- src/components/filter/Filter.js | 3 +++ src/utils/buildSelectTree.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 46b4b8ab2..9b3644e65 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -72,6 +72,9 @@ const Filter = () => { showOnlyOnMap, searchValue, types, + Object.entries(categories) + .filter(([_, enabled]) => enabled) + .map(([category]) => category), ), [typesAccess, countsById, showOnlyOnMap, searchValue, types], ) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 6144109f2..ca67db87d 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -21,6 +21,7 @@ class SelectTreeBuilder { private searchValue: string private selectedTypes: number[] private visibleTypeIds: Set + private enabledCategories: string[] constructor( typesAccess: TypesAccess, @@ -28,6 +29,7 @@ class SelectTreeBuilder { showOnlyOnMap: boolean, searchValue: string, selectedTypes: number[], + enabledCategories: string[], ) { this.typesAccess = typesAccess this.countsById = countsById @@ -35,6 +37,7 @@ class SelectTreeBuilder { this.searchValue = searchValue.toLowerCase() this.selectedTypes = selectedTypes this.visibleTypeIds = new Set() + this.enabledCategories = enabledCategories } private isCultivarWithParentInSelection( @@ -78,6 +81,10 @@ class SelectTreeBuilder { const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) + const matchesCategories = + this.enabledCategories.length === 0 || + type.categories.some((cat) => this.enabledCategories.includes(cat)) + const node: RenderTreeNode = { id: type.id, parent, @@ -89,7 +96,8 @@ class SelectTreeBuilder { isSelected: this.selectedTypes.includes(type.id), isIndeterminate: false, isDisabled: - this.searchValue !== '' && !matchesSearch && !parentMatchesSearch, + (this.searchValue !== '' && !matchesSearch && !parentMatchesSearch) || + (!matchesCategories && !parentMatchesSearch), } const children = (this.typesAccess.childrenById[type.id] || []) @@ -179,6 +187,7 @@ function buildSelectTree( showOnlyOnMap: boolean, searchValue: string, selectedTypes: number[], + enabledCategories: string[] = [], ): SelectTreeResult { const builder = new SelectTreeBuilder( typesAccess, @@ -186,6 +195,7 @@ function buildSelectTree( showOnlyOnMap, searchValue, selectedTypes, + enabledCategories, ) const tree = builder.buildRenderTree() const visibleTypeIds = builder.getVisibleTypes() From f08304b98f6e57c8eb750a69d9cdba6563d11f21 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:37:44 +0000 Subject: [PATCH 08/35] Aider prompt: Only show leaf nodes if they match one or more selected category --- src/utils/buildSelectTree.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index ca67db87d..ea4165f8e 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -106,6 +106,12 @@ class SelectTreeBuilder { ) .filter((child): child is RenderTreeNode => child !== null) + // Don't show leaf nodes that don't match categories + if (children.length === 0 && !matchesCategories) { + return null + } + + // Don't show nodes that don't match search if (!matchesSearch && !parentMatchesSearch && children.length === 0) { return null } From d121b4d8faf599c97628f28cd0a0e3693937e2d4 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:40:49 +0000 Subject: [PATCH 09/35] Aider prompt: Add logging to help me debug category logic --- src/utils/buildSelectTree.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index ea4165f8e..3ec5dc9d9 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -81,10 +81,16 @@ class SelectTreeBuilder { const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) + console.log(`Processing node ${type.id} (${type.commonName}):`) + console.log(`- Enabled categories:`, this.enabledCategories) + console.log(`- Node categories:`, type.categories) + const matchesCategories = this.enabledCategories.length === 0 || type.categories.some((cat) => this.enabledCategories.includes(cat)) + console.log(`- Matches categories: ${matchesCategories}`) + const node: RenderTreeNode = { id: type.id, parent, @@ -108,6 +114,7 @@ class SelectTreeBuilder { // Don't show leaf nodes that don't match categories if (children.length === 0 && !matchesCategories) { + console.log(`- Skipping leaf node ${type.id} - doesn't match categories`) return null } From 65fb9303648c57b005648d95445c2af8585ebff8 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:49:50 +0000 Subject: [PATCH 10/35] Checking in before running aider: Add 'parentMatchesCategories' and copy parentMatchesSearch logic --- src/components/filter/Filter.js | 2 +- src/utils/buildSelectTree.ts | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 9b3644e65..ae7cad253 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -76,7 +76,7 @@ const Filter = () => { .filter(([_, enabled]) => enabled) .map(([category]) => category), ), - [typesAccess, countsById, showOnlyOnMap, searchValue, types], + [typesAccess, countsById, showOnlyOnMap, searchValue, types, categories], ) const { t } = useTranslation() diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 3ec5dc9d9..d98508836 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -81,15 +81,9 @@ class SelectTreeBuilder { const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) - console.log(`Processing node ${type.id} (${type.commonName}):`) - console.log(`- Enabled categories:`, this.enabledCategories) - console.log(`- Node categories:`, type.categories) - - const matchesCategories = - this.enabledCategories.length === 0 || - type.categories.some((cat) => this.enabledCategories.includes(cat)) - - console.log(`- Matches categories: ${matchesCategories}`) + const matchesCategories = type.categories.some((cat) => + this.enabledCategories.includes(cat), + ) const node: RenderTreeNode = { id: type.id, @@ -112,13 +106,10 @@ class SelectTreeBuilder { ) .filter((child): child is RenderTreeNode => child !== null) - // Don't show leaf nodes that don't match categories if (children.length === 0 && !matchesCategories) { - console.log(`- Skipping leaf node ${type.id} - doesn't match categories`) return null } - // Don't show nodes that don't match search if (!matchesSearch && !parentMatchesSearch && children.length === 0) { return null } From 2b16287371bca20c3d25ba0804cf27285a592d90 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 09:50:03 +0000 Subject: [PATCH 11/35] Aider prompt: Add 'parentMatchesCategories' and copy parentMatchesSearch logic --- src/utils/buildSelectTree.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index d98508836..ce01d0b28 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -72,6 +72,7 @@ class SelectTreeBuilder { type: LocalizedType, parent: RenderTreeNode | null = null, parentMatchesSearch: boolean = false, + parentMatchesCategories: boolean = false, ): RenderTreeNode | null { const count = this.getAggregatedCount(type.id) if (this.showOnlyOnMap && count === 0) { @@ -97,12 +98,17 @@ class SelectTreeBuilder { isIndeterminate: false, isDisabled: (this.searchValue !== '' && !matchesSearch && !parentMatchesSearch) || - (!matchesCategories && !parentMatchesSearch), + (!matchesCategories && !parentMatchesCategories), } const children = (this.typesAccess.childrenById[type.id] || []) .map((childId) => - this.buildNode(this.typesAccess.getType(childId), node, matchesSearch), + this.buildNode( + this.typesAccess.getType(childId), + node, + matchesSearch, + matchesCategories, + ), ) .filter((child): child is RenderTreeNode => child !== null) From a2837043179e41ee589f590bdf8bec043a484dad Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 10:02:00 +0000 Subject: [PATCH 12/35] Checking in before running aider: Add a fifth 'no category' category, which applies only to types without any categories. It should have a checkbox, state in redux, and influence on select tree --- src/utils/buildSelectTree.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index ce01d0b28..bc3b6809e 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -112,7 +112,11 @@ class SelectTreeBuilder { ) .filter((child): child is RenderTreeNode => child !== null) - if (children.length === 0 && !matchesCategories) { + if ( + children.length === 0 && + !parentMatchesCategories && + !matchesCategories + ) { return null } From b022c80dfc92f795b890cb8f8f42640e19cc3846 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 10:02:15 +0000 Subject: [PATCH 13/35] Aider prompt: Add a fifth 'no category' category, which applies only to types without any categories. It should have a checkbox, state in redux, and influence on select tree --- src/components/filter/Filter.js | 10 ++++++++++ src/redux/filterSlice.js | 1 + src/utils/buildSelectTree.ts | 7 ++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index ae7cad253..d4bec1db8 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -166,6 +166,16 @@ const Filter = () => { label={t('invasives')} onChange={(checked) => dispatch(invasiveChanged(checked))} /> + + dispatch( + categoryChanged({ category: 'noCategory', value: checked }), + ) + } + /> ) diff --git a/src/redux/filterSlice.js b/src/redux/filterSlice.js index e3664c9ad..867e7a94c 100644 --- a/src/redux/filterSlice.js +++ b/src/redux/filterSlice.js @@ -52,6 +52,7 @@ export const filterSlice = createSlice({ freegan: true, grafter: false, honeybee: false, + noCategory: true, }, }, reducers: { diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index bc3b6809e..8d3da18c6 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -82,9 +82,10 @@ class SelectTreeBuilder { const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) - const matchesCategories = type.categories.some((cat) => - this.enabledCategories.includes(cat), - ) + const matchesCategories = + type.categories.length === 0 + ? this.enabledCategories.includes('noCategory') + : type.categories.some((cat) => this.enabledCategories.includes(cat)) const node: RenderTreeNode = { id: type.id, From 30abca21cd2ed05bdf2585e8d275e0ed9dde528c Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 12:10:55 +0000 Subject: [PATCH 14/35] Checking in before running aider: Change how we use categories to display. The logic for leaf nodes and parents treated as leaf nodes to indicate nonspecific annotation should be as currently - if type includes one or more categories, it's in - but for parents that are there for tree structure, use children's match: all children matching category means include enabled, only some children matching category means include disabled --- src/components/filter/Filter.js | 1 + src/redux/filterSlice.js | 2 +- src/utils/buildSelectTree.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index d4bec1db8..a7ca7ffcc 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -78,6 +78,7 @@ const Filter = () => { ), [typesAccess, countsById, showOnlyOnMap, searchValue, types, categories], ) + console.log({ selectTree, visibleTypeIds }) const { t } = useTranslation() return ( diff --git a/src/redux/filterSlice.js b/src/redux/filterSlice.js index 867e7a94c..3b3a98ba0 100644 --- a/src/redux/filterSlice.js +++ b/src/redux/filterSlice.js @@ -52,7 +52,7 @@ export const filterSlice = createSlice({ freegan: true, grafter: false, honeybee: false, - noCategory: true, + noCategory: false, }, }, reducers: { diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 8d3da18c6..ba4150566 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -9,6 +9,7 @@ interface RenderTreeNode { count: number searchLabel: string children: RenderTreeNode[] + categories: string[] isSelected: boolean isIndeterminate: boolean isDisabled: boolean @@ -95,6 +96,7 @@ class SelectTreeBuilder { count, searchLabel, children: [], + categories: type.categories, isSelected: this.selectedTypes.includes(type.id), isIndeterminate: false, isDisabled: From 0ea6db9bf281011bf9cfad07054770a3fccd4d39 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 13:18:52 +0000 Subject: [PATCH 15/35] Aider prompt: Add logging that will help me understand what happens when a parent matches a category but child doesn't --- src/utils/buildSelectTree.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index ba4150566..b59c1849f 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -88,6 +88,13 @@ class SelectTreeBuilder { ? this.enabledCategories.includes('noCategory') : type.categories.some((cat) => this.enabledCategories.includes(cat)) + console.log(`Node ${type.commonName} (${type.id}):`, { + categories: type.categories, + matchesCategories, + parentMatchesCategories, + enabledCategories: this.enabledCategories, + }) + const node: RenderTreeNode = { id: type.id, parent, @@ -120,6 +127,11 @@ class SelectTreeBuilder { !parentMatchesCategories && !matchesCategories ) { + console.log(`Filtering out leaf node ${type.commonName} (${type.id}):`, { + reason: 'No children and neither parent nor self matches categories', + parentMatchesCategories, + matchesCategories, + }) return null } From d22ec94b3a78421450c4626276cceb3798b724a2 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Wed, 13 Nov 2024 16:37:36 +0000 Subject: [PATCH 16/35] x --- src/utils/buildSelectTree.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index b59c1849f..c011338fb 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -88,13 +88,6 @@ class SelectTreeBuilder { ? this.enabledCategories.includes('noCategory') : type.categories.some((cat) => this.enabledCategories.includes(cat)) - console.log(`Node ${type.commonName} (${type.id}):`, { - categories: type.categories, - matchesCategories, - parentMatchesCategories, - enabledCategories: this.enabledCategories, - }) - const node: RenderTreeNode = { id: type.id, parent, @@ -127,11 +120,6 @@ class SelectTreeBuilder { !parentMatchesCategories && !matchesCategories ) { - console.log(`Filtering out leaf node ${type.commonName} (${type.id}):`, { - reason: 'No children and neither parent nor self matches categories', - parentMatchesCategories, - matchesCategories, - }) return null } @@ -156,7 +144,7 @@ class SelectTreeBuilder { : type.scientificName const ownCount = this.getCount(type.id) - if (children.length && ownCount > 0 && matchesSearch) { + if (children.length && ownCount > 0 && matchesSearch && matchesCategories) { const childNode: RenderTreeNode = { ...node, id: -type.id, // Use negative ID to ensure uniqueness @@ -166,7 +154,7 @@ class SelectTreeBuilder { children: [], isSelected: this.selectedTypes.includes(type.id), isIndeterminate: false, - isDisabled: !matchesSearch, + isDisabled: false, } node.children.unshift(childNode) } else if (children.length === 0) { From f89e1b50969577d093ec810e12aff25fc2d58ed9 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Thu, 14 Nov 2024 14:36:14 +0000 Subject: [PATCH 17/35] Aider prompt: Move code for matching enabled categories to a helper method --- src/utils/buildSelectTree.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index c011338fb..7bfe15bb9 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -60,6 +60,12 @@ class SelectTreeBuilder { ) } + private matchesEnabledCategories(type: LocalizedType): boolean { + return type.categories.length === 0 + ? this.enabledCategories.includes('noCategory') + : type.categories.some((cat) => this.enabledCategories.includes(cat)) + } + buildRenderTree(): RenderTreeNode[] { const rootNodes = this.typesAccess.localizedTypes.filter( (type) => type.parentId === 0, @@ -83,10 +89,7 @@ class SelectTreeBuilder { const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) - const matchesCategories = - type.categories.length === 0 - ? this.enabledCategories.includes('noCategory') - : type.categories.some((cat) => this.enabledCategories.includes(cat)) + const matchesCategories = this.matchesEnabledCategories(type) const node: RenderTreeNode = { id: type.id, From a5124b8665436673f5806017c96402e25ff1ca76 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Thu, 14 Nov 2024 14:37:21 +0000 Subject: [PATCH 18/35] Aider prompt: Add a method to buildSelectTree, getAggregatedCountInEnabledCategories, and log out the result - we'll use it in a second --- src/utils/buildSelectTree.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 7bfe15bb9..d022cf43d 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -194,6 +194,24 @@ class SelectTreeBuilder { getVisibleTypes(): number[] { return Array.from(this.visibleTypeIds) } + + private getAggregatedCountInEnabledCategories(id: number): number { + const type = this.typesAccess.getType(id) + let count = 0 + + // Only include count if type matches enabled categories + if (this.matchesEnabledCategories(type)) { + count = this.getCount(id) + } + + // Recursively add counts from children + const children = this.typesAccess.childrenById[id] || [] + for (const childId of children) { + count += this.getAggregatedCountInEnabledCategories(childId) + } + + return count + } } interface SelectTreeResult { @@ -219,6 +237,20 @@ function buildSelectTree( ) const tree = builder.buildRenderTree() const visibleTypeIds = builder.getVisibleTypes() + + // Log aggregated counts for root nodes + const rootNodes = typesAccess.localizedTypes.filter( + (type) => type.parentId === 0, + ) + rootNodes.forEach((node) => { + console.log( + `Aggregated count for ${ + node.scientificName || node.commonName + } in enabled categories:`, + builder.getAggregatedCountInEnabledCategories(node.id), + ) + }) + return { tree, visibleTypeIds } } From d56057e42a90bf8ad3f8af9aa26c0f0303e7a96d Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Thu, 14 Nov 2024 15:38:11 +0000 Subject: [PATCH 19/35] Checking in before running aider: Display category checkboxes in a 4x2 grid --- src/components/filter/Filter.js | 94 ++++++++++++++++++--------------- src/utils/buildSelectTree.ts | 54 +++++++------------ 2 files changed, 70 insertions(+), 78 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index a7ca7ffcc..4029952c2 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -50,6 +50,12 @@ const MuniAndInvasiveCheckboxFilters = styled.div` } ` +const CategoryCheckboxes = styled.div` + margin-bottom: 0.5em; + display: flex; + gap: 5px; +` + const Filter = () => { const [searchValue, setSearchValue] = useState('') const [showOnlyOnMap, setShowOnlyOnMap] = useState(true) @@ -85,6 +91,52 @@ const Filter = () => { <>
{t('glossary.types')} + + + dispatch(categoryChanged({ category: 'forager', value: checked })) + } + /> + + dispatch(categoryChanged({ category: 'freegan', value: checked })) + } + /> + + dispatch(categoryChanged({ category: 'grafter', value: checked })) + } + /> + + dispatch( + categoryChanged({ category: 'honeybee', value: checked }), + ) + } + /> + + dispatch( + categoryChanged({ category: 'noCategory', value: checked }), + ) + } + /> + setSearchValueDebounced(e.target.value)} placeholder={t('type')} @@ -123,38 +175,6 @@ const Filter = () => { )}
- - dispatch(categoryChanged({ category: 'forager', value: checked })) - } - /> - - dispatch(categoryChanged({ category: 'freegan', value: checked })) - } - /> - - dispatch(categoryChanged({ category: 'grafter', value: checked })) - } - /> - - dispatch(categoryChanged({ category: 'honeybee', value: checked })) - } - /> { label={t('invasives')} onChange={(checked) => dispatch(invasiveChanged(checked))} /> - - dispatch( - categoryChanged({ category: 'noCategory', value: checked }), - ) - } - /> ) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index d022cf43d..34786356e 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -79,18 +79,27 @@ class SelectTreeBuilder { type: LocalizedType, parent: RenderTreeNode | null = null, parentMatchesSearch: boolean = false, - parentMatchesCategories: boolean = false, ): RenderTreeNode | null { - const count = this.getAggregatedCount(type.id) - if (this.showOnlyOnMap && count === 0) { - return null + const countInEnabledCategories = this.getAggregatedCountInEnabledCategories( + type.id, + ) + const matchesCategories = this.matchesEnabledCategories(type) + + if (this.showOnlyOnMap) { + if (countInEnabledCategories === 0) { + return null + } + } else { + if (!matchesCategories) { + return null + } } + const count = this.getAggregatedCount(type.id) + const searchLabel = `${type.commonName} ${type.scientificName}`.trim() const matchesSearch = !this.searchValue || searchLabel.toLowerCase().includes(this.searchValue) - const matchesCategories = this.matchesEnabledCategories(type) - const node: RenderTreeNode = { id: type.id, parent, @@ -104,28 +113,15 @@ class SelectTreeBuilder { isIndeterminate: false, isDisabled: (this.searchValue !== '' && !matchesSearch && !parentMatchesSearch) || - (!matchesCategories && !parentMatchesCategories), + countInEnabledCategories < count, } const children = (this.typesAccess.childrenById[type.id] || []) .map((childId) => - this.buildNode( - this.typesAccess.getType(childId), - node, - matchesSearch, - matchesCategories, - ), + this.buildNode(this.typesAccess.getType(childId), node, matchesSearch), ) .filter((child): child is RenderTreeNode => child !== null) - if ( - children.length === 0 && - !parentMatchesCategories && - !matchesCategories - ) { - return null - } - if (!matchesSearch && !parentMatchesSearch && children.length === 0) { return null } @@ -150,7 +146,7 @@ class SelectTreeBuilder { if (children.length && ownCount > 0 && matchesSearch && matchesCategories) { const childNode: RenderTreeNode = { ...node, - id: -type.id, // Use negative ID to ensure uniqueness + id: -type.id, parent: node, count: ownCount, value: type.id, @@ -164,7 +160,6 @@ class SelectTreeBuilder { node.value = type.id } - // Calculate isSelected and isIndeterminate if (node.children.length > 0) { const allChildrenSelected = node.children.every( (child) => child.isSelected, @@ -238,19 +233,6 @@ function buildSelectTree( const tree = builder.buildRenderTree() const visibleTypeIds = builder.getVisibleTypes() - // Log aggregated counts for root nodes - const rootNodes = typesAccess.localizedTypes.filter( - (type) => type.parentId === 0, - ) - rootNodes.forEach((node) => { - console.log( - `Aggregated count for ${ - node.scientificName || node.commonName - } in enabled categories:`, - builder.getAggregatedCountInEnabledCategories(node.id), - ) - }) - return { tree, visibleTypeIds } } From 4ca25df2c51d3eb24fa27800192b5b53f916dde3 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Thu, 14 Nov 2024 15:38:22 +0000 Subject: [PATCH 20/35] Aider prompt: Display category checkboxes in a 4x2 grid --- src/components/filter/Filter.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 4029952c2..24048b00a 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -52,8 +52,11 @@ const MuniAndInvasiveCheckboxFilters = styled.div` const CategoryCheckboxes = styled.div` margin-bottom: 0.5em; - display: flex; - gap: 5px; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, auto); + gap: 8px; + width: 100%; ` const Filter = () => { From 67ff5e5e8a8fafcc4b7c8737cdc10f9ce283845c Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Thu, 14 Nov 2024 18:25:49 +0000 Subject: [PATCH 21/35] x --- src/components/filter/Filter.js | 27 +++++++++++++-------------- src/redux/mapSlice.js | 6 ++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 24048b00a..82388a4b4 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -51,12 +51,10 @@ const MuniAndInvasiveCheckboxFilters = styled.div` ` const CategoryCheckboxes = styled.div` - margin-bottom: 0.5em; + margin-bottom: 0.25em; display: grid; grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(2, auto); - gap: 8px; - width: 100%; + gap: 0.25em; ` const Filter = () => { @@ -129,17 +127,18 @@ const Filter = () => { ) } /> - - dispatch( - categoryChanged({ category: 'noCategory', value: checked }), - ) - } - /> + + dispatch( + categoryChanged({ category: 'noCategory', value: checked }), + ) + } + /> setSearchValueDebounced(e.target.value)} placeholder={t('type')} diff --git a/src/redux/mapSlice.js b/src/redux/mapSlice.js index f9666119d..ef4767ef2 100644 --- a/src/redux/mapSlice.js +++ b/src/redux/mapSlice.js @@ -14,6 +14,12 @@ export const fetchMapLocations = createAsyncThunk( const state = getState() const { types, muni, invasive } = state.filter const { lastMapView } = state.viewport + /* + * TODO: types are null on first load + * + * this is then getting us locations which are not meant to be selected by default + * + */ if (lastMapView) { const { bounds, zoom, center: _ } = lastMapView return await getLocations( From c5ff8470aa208adb49a36a6d97e72094d7dc7b04 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 10:41:10 +0000 Subject: [PATCH 22/35] x --- src/components/filter/Filter.js | 2 +- src/utils/buildSelectTree.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 82388a4b4..848bf6e27 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -132,7 +132,7 @@ const Filter = () => { style={{ marginBottom: '0.5em' }} field="noCategory" value={categories.noCategory} - label="Pending" + label="Other" onChange={(checked) => dispatch( categoryChanged({ category: 'noCategory', value: checked }), diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 34786356e..9b2400776 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -95,6 +95,7 @@ class SelectTreeBuilder { } } const count = this.getAggregatedCount(type.id) + const ownCount = this.getCount(type.id) const searchLabel = `${type.commonName} ${type.scientificName}`.trim() const matchesSearch = @@ -113,7 +114,8 @@ class SelectTreeBuilder { isIndeterminate: false, isDisabled: (this.searchValue !== '' && !matchesSearch && !parentMatchesSearch) || - countInEnabledCategories < count, + (ownCount < countInEnabledCategories && + countInEnabledCategories < count), } const children = (this.typesAccess.childrenById[type.id] || []) @@ -142,7 +144,6 @@ class SelectTreeBuilder { ? type.scientificName?.substring(cultivarIndex ?? -1) : type.scientificName - const ownCount = this.getCount(type.id) if (children.length && ownCount > 0 && matchesSearch && matchesCategories) { const childNode: RenderTreeNode = { ...node, From b2415ca462e9f15c96202a87923ee0e4cf15eee8 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 10:44:12 +0000 Subject: [PATCH 23/35] Checking in before running aider: Add a 'isVisible' property to RenderTreeNode. Then don't return null, but instead set isVisible to false. --- src/components/filter/TreeSelectView.js | 98 +++++++++++++------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/components/filter/TreeSelectView.js b/src/components/filter/TreeSelectView.js index 97404ef8c..ec6922efc 100644 --- a/src/components/filter/TreeSelectView.js +++ b/src/components/filter/TreeSelectView.js @@ -113,54 +113,58 @@ const TreeSelectView = ({ const isExpanded = Boolean(expandedNodes.has(node.id) | isDisabled) return ( - - - - {node.children.length > 0 ? ( - handleToggle(node.id)} + node.isVisible && ( + + + + {node.children.length > 0 ? ( + handleToggle(node.id)} + disabled={isDisabled} + > + + + ) : ( + + )} + { + if (el) { + el.indeterminate = node.isIndeterminate + } + }} + onChange={() => !isDisabled && handleCheckboxChange(node)} disabled={isDisabled} - > - - - ) : ( - - )} - { - if (el) { - el.indeterminate = node.isIndeterminate - } - }} - onChange={() => !isDisabled && handleCheckboxChange(node)} - disabled={isDisabled} - /> - - - {node.commonName && ( - {node.commonName} - )} - {node.scientificName && ( - - {node.scientificName} - - )} - ({node.count}) - - - {isExpanded && - node.children.map((child) => renderNode(child, level + 1))} - + /> + + + {node.commonName && ( + + {node.commonName} + + )} + {node.scientificName && ( + + {node.scientificName} + + )} + ({node.count}) + + + {isExpanded && + node.children.map((child) => renderNode(child, level + 1))} + + ) ) } From 7c122d892eb983e729b6e114e240375d607ca16d Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 10:44:29 +0000 Subject: [PATCH 24/35] Aider prompt: Add a 'isVisible' property to RenderTreeNode. Then don't return null, but instead set isVisible to false. --- src/utils/buildSelectTree.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 9b2400776..8e9d8b676 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -13,6 +13,7 @@ interface RenderTreeNode { isSelected: boolean isIndeterminate: boolean isDisabled: boolean + isVisible: boolean } class SelectTreeBuilder { @@ -72,7 +73,7 @@ class SelectTreeBuilder { ) return rootNodes .map((node) => this.buildNode(node, null, false)) - .filter((node): node is RenderTreeNode => node !== null) + .filter((node) => node.isVisible) } private buildNode( @@ -85,15 +86,9 @@ class SelectTreeBuilder { ) const matchesCategories = this.matchesEnabledCategories(type) - if (this.showOnlyOnMap) { - if (countInEnabledCategories === 0) { - return null - } - } else { - if (!matchesCategories) { - return null - } - } + const isVisible = this.showOnlyOnMap + ? countInEnabledCategories > 0 + : matchesCategories const count = this.getAggregatedCount(type.id) const ownCount = this.getCount(type.id) @@ -105,6 +100,7 @@ class SelectTreeBuilder { id: type.id, parent, commonName: type.commonName, + isVisible, scientificName: type.scientificName, count, searchLabel, @@ -122,10 +118,11 @@ class SelectTreeBuilder { .map((childId) => this.buildNode(this.typesAccess.getType(childId), node, matchesSearch), ) - .filter((child): child is RenderTreeNode => child !== null) + .filter((child) => child.isVisible) if (!matchesSearch && !parentMatchesSearch && children.length === 0) { - return null + node.isVisible = false + return node } if (!node.isDisabled) { From 0cfbe110471749bace46df6efb49b46c2df8d7b2 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 10:58:29 +0000 Subject: [PATCH 25/35] x --- src/components/filter/Filter.js | 1 - src/utils/buildSelectTree.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 848bf6e27..c233d6d5d 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -85,7 +85,6 @@ const Filter = () => { ), [typesAccess, countsById, showOnlyOnMap, searchValue, types, categories], ) - console.log({ selectTree, visibleTypeIds }) const { t } = useTranslation() return ( diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 8e9d8b676..8d2e5de0e 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -80,7 +80,7 @@ class SelectTreeBuilder { type: LocalizedType, parent: RenderTreeNode | null = null, parentMatchesSearch: boolean = false, - ): RenderTreeNode | null { + ): RenderTreeNode { const countInEnabledCategories = this.getAggregatedCountInEnabledCategories( type.id, ) @@ -114,11 +114,10 @@ class SelectTreeBuilder { countInEnabledCategories < count), } - const children = (this.typesAccess.childrenById[type.id] || []) - .map((childId) => + const children = (this.typesAccess.childrenById[type.id] || []).map( + (childId) => this.buildNode(this.typesAccess.getType(childId), node, matchesSearch), - ) - .filter((child) => child.isVisible) + ) if (!matchesSearch && !parentMatchesSearch && children.length === 0) { node.isVisible = false @@ -160,10 +159,11 @@ class SelectTreeBuilder { if (node.children.length > 0) { const allChildrenSelected = node.children.every( - (child) => child.isSelected, + (child) => !child.isVisible || child.isSelected, ) const someChildrenSelected = node.children.some( - (child) => child.isSelected || child.isIndeterminate, + (child) => + !child.isVisible || child.isSelected || child.isIndeterminate, ) node.isSelected = allChildrenSelected node.isIndeterminate = !allChildrenSelected && someChildrenSelected From 5deeed29d8f99eb90d8d23a941fa2f9c76ac0e5c Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 13:21:00 +0000 Subject: [PATCH 26/35] Maybe working now --- src/components/filter/Filter.js | 1 + src/components/filter/TreeSelectView.js | 2 +- src/redux/filterSlice.js | 7 +---- src/utils/buildSelectTree.ts | 34 ++++++++++++++++--------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index c233d6d5d..848bf6e27 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -85,6 +85,7 @@ const Filter = () => { ), [typesAccess, countsById, showOnlyOnMap, searchValue, types, categories], ) + console.log({ selectTree, visibleTypeIds }) const { t } = useTranslation() return ( diff --git a/src/components/filter/TreeSelectView.js b/src/components/filter/TreeSelectView.js index ec6922efc..75af64751 100644 --- a/src/components/filter/TreeSelectView.js +++ b/src/components/filter/TreeSelectView.js @@ -117,7 +117,7 @@ const TreeSelectView = ({ - {node.children.length > 0 ? ( + {node.children.some((c) => c.isVisible) ? ( handleToggle(node.id)} disabled={isDisabled} diff --git a/src/redux/filterSlice.js b/src/redux/filterSlice.js index 3b3a98ba0..e9181e8e6 100644 --- a/src/redux/filterSlice.js +++ b/src/redux/filterSlice.js @@ -90,12 +90,7 @@ export const filterSlice = createSlice({ [fetchAndLocalizeTypes.fulfilled]: (state, action) => { const typesAccess = action.payload - const selectedCategories = Object.entries(state.categories) - .filter(([_, isSelected]) => isSelected) - .map(([category]) => category) - state.types = typesAccess - .selectableTypesWithCategories(...selectedCategories) - .map((t) => t.id) + state.types = typesAccess.selectableTypes().map((t) => t.id) }, }, }) diff --git a/src/utils/buildSelectTree.ts b/src/utils/buildSelectTree.ts index 8d2e5de0e..cfe178037 100644 --- a/src/utils/buildSelectTree.ts +++ b/src/utils/buildSelectTree.ts @@ -110,8 +110,7 @@ class SelectTreeBuilder { isIndeterminate: false, isDisabled: (this.searchValue !== '' && !matchesSearch && !parentMatchesSearch) || - (ownCount < countInEnabledCategories && - countInEnabledCategories < count), + (ownCount < count && countInEnabledCategories < count), } const children = (this.typesAccess.childrenById[type.id] || []).map( @@ -119,12 +118,16 @@ class SelectTreeBuilder { this.buildNode(this.typesAccess.getType(childId), node, matchesSearch), ) - if (!matchesSearch && !parentMatchesSearch && children.length === 0) { + if ( + !matchesSearch && + !parentMatchesSearch && + children.every((c) => !c.isVisible) + ) { node.isVisible = false return node } - if (!node.isDisabled) { + if (!node.isDisabled && node.isVisible) { this.visibleTypeIds.add(type.id) } @@ -140,7 +143,12 @@ class SelectTreeBuilder { ? type.scientificName?.substring(cultivarIndex ?? -1) : type.scientificName - if (children.length && ownCount > 0 && matchesSearch && matchesCategories) { + if ( + children.some((c) => c.isVisible) && + ownCount > 0 && + matchesSearch && + matchesCategories + ) { const childNode: RenderTreeNode = { ...node, id: -type.id, @@ -153,17 +161,19 @@ class SelectTreeBuilder { isDisabled: false, } node.children.unshift(childNode) - } else if (children.length === 0) { + this.visibleTypeIds.add(type.id) + } else if (!children.some((c) => c.isVisible)) { node.value = type.id } - if (node.children.length > 0) { - const allChildrenSelected = node.children.every( - (child) => !child.isVisible || child.isSelected, + const visibleChildren = node.children.filter((c) => c.isVisible) + + if (visibleChildren.length > 0) { + const allChildrenSelected = visibleChildren.every( + (child) => child.isSelected, ) - const someChildrenSelected = node.children.some( - (child) => - !child.isVisible || child.isSelected || child.isIndeterminate, + const someChildrenSelected = visibleChildren.some( + (child) => child.isSelected || child.isIndeterminate, ) node.isSelected = allChildrenSelected node.isIndeterminate = !allChildrenSelected && someChildrenSelected From df3a3ce5209961a6bb0c56b7b2a15a15b57dd10d Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 13:23:11 +0000 Subject: [PATCH 27/35] Checking in before running aider: in fetchMapLocations, filter the types so that only types for currently selected categories are passed into selectParams --- src/redux/mapSlice.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/redux/mapSlice.js b/src/redux/mapSlice.js index ef4767ef2..f9666119d 100644 --- a/src/redux/mapSlice.js +++ b/src/redux/mapSlice.js @@ -14,12 +14,6 @@ export const fetchMapLocations = createAsyncThunk( const state = getState() const { types, muni, invasive } = state.filter const { lastMapView } = state.viewport - /* - * TODO: types are null on first load - * - * this is then getting us locations which are not meant to be selected by default - * - */ if (lastMapView) { const { bounds, zoom, center: _ } = lastMapView return await getLocations( From c68c3441c1faec09062b49b9f2fefe4129729b33 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 13:29:19 +0000 Subject: [PATCH 28/35] Checking in before running aider: Since locations/clusters on the map depend on categories, make sure we refetch after category checkboxes are interacted with --- src/redux/mapSlice.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/redux/mapSlice.js b/src/redux/mapSlice.js index f9666119d..3748667ab 100644 --- a/src/redux/mapSlice.js +++ b/src/redux/mapSlice.js @@ -12,13 +12,30 @@ export const fetchMapLocations = createAsyncThunk( 'map/fetchMapLocations', async (_, { getState }) => { const state = getState() - const { types, muni, invasive } = state.filter + const { types, muni, invasive, categories } = state.filter const { lastMapView } = state.viewport + const { typesAccess } = state.type + if (lastMapView) { const { bounds, zoom, center: _ } = lastMapView + + const filteredTypes = types?.filter((typeId) => { + const type = typesAccess.getType(typeId) + return type.categories.length === 0 + ? categories.noCategory + : type.categories.some((cat) => categories[cat]) + }) + return await getLocations( selectParams( - { types, muni, invasive, bounds, zoom, center: undefined }, + { + types: filteredTypes, + muni, + invasive, + bounds, + zoom, + center: undefined, + }, { limit: 250 }, ), ) @@ -32,13 +49,21 @@ export const fetchMapClusters = createAsyncThunk( 'map/fetchMapClusters', async (_, { getState }) => { const state = getState() - const { types, muni, invasive } = state.filter + const { types, muni, invasive, categories } = state.filter const { lastMapView } = state.viewport + const { typesAccess } = state.type if (lastMapView) { const { bounds, zoom, center: _ } = lastMapView + + const filteredTypes = types?.filter((typeId) => { + const type = typesAccess.getType(typeId) + return type.categories.length === 0 + ? categories.noCategory + : type.categories.some((cat) => categories[cat]) + }) return await getClusters( selectParams({ - types, + types: filteredTypes, muni, invasive, bounds, From a85fa047ec6297c4f0c4f4fb919114db28859c16 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 13:29:37 +0000 Subject: [PATCH 29/35] Aider prompt: Since locations/clusters on the map depend on categories, make sure we refetch after category checkboxes are interacted with --- src/components/filter/Filter.js | 34 +++++++++++++++------------------ src/redux/viewChange.js | 5 +++++ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 848bf6e27..3aa1ffabe 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -97,35 +97,33 @@ const Filter = () => { field="forager" value={categories.forager} label="Forager" - onChange={(checked) => - dispatch(categoryChanged({ category: 'forager', value: checked })) - } + onChange={(checked) => { + dispatch(categoryChanged('forager', checked)) + }} /> - dispatch(categoryChanged({ category: 'freegan', value: checked })) - } + onChange={(checked) => { + dispatch(categoryChanged('freegan', checked)) + }} /> - dispatch(categoryChanged({ category: 'grafter', value: checked })) - } + onChange={(checked) => { + dispatch(categoryChanged('grafter', checked)) + }} /> - dispatch( - categoryChanged({ category: 'honeybee', value: checked }), - ) - } + onChange={(checked) => { + dispatch(categoryChanged('honeybee', checked)) + }} /> { field="noCategory" value={categories.noCategory} label="Other" - onChange={(checked) => - dispatch( - categoryChanged({ category: 'noCategory', value: checked }), - ) - } + onChange={(checked) => { + dispatch(categoryChanged('noCategory', checked)) + }} /> setSearchValueDebounced(e.target.value)} diff --git a/src/redux/viewChange.js b/src/redux/viewChange.js index 16d2a6eb1..332eff5f2 100644 --- a/src/redux/viewChange.js +++ b/src/redux/viewChange.js @@ -37,3 +37,8 @@ export const selectionChanged = (types) => (dispatch) => { dispatch(updateSelection({ types })) dispatch(fetchLocations()) } + +export const categoryChanged = (category, value) => (dispatch) => { + dispatch(updateSelection({ categories: { [category]: value } })) + dispatch(fetchLocations()) +} From 1bfc9517c192cfe14a818c8e517f1a80422ccc4a Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 14:38:11 +0000 Subject: [PATCH 30/35] Checking in before running aider: There's a bug with categories being reset. Solve it by defining categoriesChanged and using that in Filter.js --- src/components/filter/Filter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 3aa1ffabe..98aff997f 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components/macro' -import { categoryChanged } from '../../redux/filterSlice' import { + categoryChanged, invasiveChanged, muniChanged, selectionChanged, @@ -85,7 +85,6 @@ const Filter = () => { ), [typesAccess, countsById, showOnlyOnMap, searchValue, types, categories], ) - console.log({ selectTree, visibleTypeIds }) const { t } = useTranslation() return ( From daac47b0a219d731d5dcc565e0be453ef36e0ec4 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 14:56:04 +0000 Subject: [PATCH 31/35] Checking in before running aider: Instead of categories as checkboxes, do them as a multiselect --- src/redux/viewChange.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/redux/viewChange.js b/src/redux/viewChange.js index 332eff5f2..a68b39435 100644 --- a/src/redux/viewChange.js +++ b/src/redux/viewChange.js @@ -38,7 +38,15 @@ export const selectionChanged = (types) => (dispatch) => { dispatch(fetchLocations()) } -export const categoryChanged = (category, value) => (dispatch) => { - dispatch(updateSelection({ categories: { [category]: value } })) +export const categoryChanged = (category, value) => (dispatch, getState) => { + const currentCategories = getState().filter.categories + dispatch( + updateSelection({ + categories: { + ...currentCategories, + [category]: value, + }, + }), + ) dispatch(fetchLocations()) } From 23ddf09da30e2f8bf6f74a0ce5067d77f5207e99 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 14:56:24 +0000 Subject: [PATCH 32/35] Aider prompt: Instead of categories as checkboxes, do them as a multiselect --- src/components/filter/Filter.js | 81 ++++++++++++++------------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 98aff997f..661d65074 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -50,11 +50,18 @@ const MuniAndInvasiveCheckboxFilters = styled.div` } ` -const CategoryCheckboxes = styled.div` - margin-bottom: 0.25em; - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.25em; +const CategorySelect = styled.select` + width: 100%; + padding: 8px; + margin-bottom: 0.5em; + border: 1px solid ${({ theme }) => theme.border}; + border-radius: 4px; + background-color: ${({ theme }) => theme.background}; + color: ${({ theme }) => theme.text}; + + option { + padding: 8px; + } ` const Filter = () => { @@ -91,49 +98,29 @@ const Filter = () => { <>
{t('glossary.types')} - - { - dispatch(categoryChanged('forager', checked)) - }} - /> - { - dispatch(categoryChanged('freegan', checked)) - }} - /> - { - dispatch(categoryChanged('grafter', checked)) - }} - /> - { - dispatch(categoryChanged('honeybee', checked)) - }} - /> - - { - dispatch(categoryChanged('noCategory', checked)) + enabled) + .map(([category]) => category)} + onChange={(e) => { + const selectedOptions = Array.from( + e.target.selectedOptions, + (option) => option.value, + ) + Object.keys(categories).forEach((category) => { + dispatch( + categoryChanged(category, selectedOptions.includes(category)), + ) + }) }} - /> + > + + + + + + setSearchValueDebounced(e.target.value)} placeholder={t('type')} From d4189bff7d76e009c3290de8b62f63cf478cd585 Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 15:00:28 +0000 Subject: [PATCH 33/35] Checking in before running aider: Make sure the selected categories are visible - give them a background color --- src/components/filter/CategorySelect.js | 50 +++++++++++++++++++ src/components/filter/Filter.js | 42 +++------------- .../components/filter/CategorySelect.js | 50 +++++++++++++++++++ 3 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 src/components/filter/CategorySelect.js create mode 100644 src/components/filter/components/filter/CategorySelect.js diff --git a/src/components/filter/CategorySelect.js b/src/components/filter/CategorySelect.js new file mode 100644 index 000000000..fe3f8c4a0 --- /dev/null +++ b/src/components/filter/CategorySelect.js @@ -0,0 +1,50 @@ +import styled from 'styled-components/macro' + +const TagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +` + +const Tag = styled.div` + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: ${({ theme, selected }) => + selected ? theme.primary : theme.background}; + color: ${({ theme, selected }) => (selected ? theme.white : theme.text)}; + border: 1px solid ${({ theme }) => theme.border}; + border-radius: 16px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: ${({ theme }) => theme.primaryLight}; + color: ${({ theme }) => theme.white}; + } +` + +const CategorySelect = ({ categories, onChange }) => { + const toggleCategory = (category) => { + const newCategories = { ...categories } + newCategories[category] = !newCategories[category] + onChange(newCategories) + } + + return ( + + {Object.entries(categories).map(([category, enabled]) => ( + toggleCategory(category)} + > + {category.charAt(0).toUpperCase() + category.slice(1)} + + ))} + + ) +} + +export default CategorySelect diff --git a/src/components/filter/Filter.js b/src/components/filter/Filter.js index 661d65074..5b49b6655 100644 --- a/src/components/filter/Filter.js +++ b/src/components/filter/Filter.js @@ -12,6 +12,7 @@ import { } from '../../redux/viewChange' import buildSelectTree from '../../utils/buildSelectTree' import Input from '../ui/Input' +import CategorySelect from './CategorySelect' import FilterButtons from './FilterButtons' import LabeledCheckbox from './LabeledCheckbox' import RCTreeSelectSkeleton from './RCTreeSelectSkeleton' @@ -50,20 +51,6 @@ const MuniAndInvasiveCheckboxFilters = styled.div` } ` -const CategorySelect = styled.select` - width: 100%; - padding: 8px; - margin-bottom: 0.5em; - border: 1px solid ${({ theme }) => theme.border}; - border-radius: 4px; - background-color: ${({ theme }) => theme.background}; - color: ${({ theme }) => theme.text}; - - option { - padding: 8px; - } -` - const Filter = () => { const [searchValue, setSearchValue] = useState('') const [showOnlyOnMap, setShowOnlyOnMap] = useState(true) @@ -99,28 +86,15 @@ const Filter = () => {
{t('glossary.types')} enabled) - .map(([category]) => category)} - onChange={(e) => { - const selectedOptions = Array.from( - e.target.selectedOptions, - (option) => option.value, - ) - Object.keys(categories).forEach((category) => { - dispatch( - categoryChanged(category, selectedOptions.includes(category)), - ) + categories={categories} + onChange={(newCategories) => { + Object.entries(newCategories).forEach(([category, enabled]) => { + if (categories[category] !== enabled) { + dispatch(categoryChanged(category, enabled)) + } }) }} - > - - - - - - + /> setSearchValueDebounced(e.target.value)} placeholder={t('type')} diff --git a/src/components/filter/components/filter/CategorySelect.js b/src/components/filter/components/filter/CategorySelect.js new file mode 100644 index 000000000..fe3f8c4a0 --- /dev/null +++ b/src/components/filter/components/filter/CategorySelect.js @@ -0,0 +1,50 @@ +import styled from 'styled-components/macro' + +const TagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +` + +const Tag = styled.div` + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: ${({ theme, selected }) => + selected ? theme.primary : theme.background}; + color: ${({ theme, selected }) => (selected ? theme.white : theme.text)}; + border: 1px solid ${({ theme }) => theme.border}; + border-radius: 16px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: ${({ theme }) => theme.primaryLight}; + color: ${({ theme }) => theme.white}; + } +` + +const CategorySelect = ({ categories, onChange }) => { + const toggleCategory = (category) => { + const newCategories = { ...categories } + newCategories[category] = !newCategories[category] + onChange(newCategories) + } + + return ( + + {Object.entries(categories).map(([category, enabled]) => ( + toggleCategory(category)} + > + {category.charAt(0).toUpperCase() + category.slice(1)} + + ))} + + ) +} + +export default CategorySelect From fb35acc2ed33b3001bab5698147f3432f09177ad Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 15:00:41 +0000 Subject: [PATCH 34/35] Aider prompt: Make sure the selected categories are visible - give them a background color --- src/components/filter/CategorySelect.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/filter/CategorySelect.js b/src/components/filter/CategorySelect.js index fe3f8c4a0..3f2eda5fc 100644 --- a/src/components/filter/CategorySelect.js +++ b/src/components/filter/CategorySelect.js @@ -14,14 +14,17 @@ const Tag = styled.div` background: ${({ theme, selected }) => selected ? theme.primary : theme.background}; color: ${({ theme, selected }) => (selected ? theme.white : theme.text)}; - border: 1px solid ${({ theme }) => theme.border}; + border: 1px solid + ${({ theme, selected }) => (selected ? theme.primary : theme.border)}; border-radius: 16px; cursor: pointer; transition: all 0.2s; + font-weight: ${({ selected }) => (selected ? 'bold' : 'normal')}; &:hover { background: ${({ theme }) => theme.primaryLight}; color: ${({ theme }) => theme.white}; + border-color: ${({ theme }) => theme.primaryLight}; } ` From c456d658a6746a913a3cbbbb6ca0ac7d7d6acdbf Mon Sep 17 00:00:00 2001 From: Wojtek Bazant Date: Mon, 18 Nov 2024 15:12:08 +0000 Subject: [PATCH 35/35] x --- src/components/filter/CategorySelect.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/filter/CategorySelect.js b/src/components/filter/CategorySelect.js index 3f2eda5fc..bfd400d61 100644 --- a/src/components/filter/CategorySelect.js +++ b/src/components/filter/CategorySelect.js @@ -12,14 +12,13 @@ const Tag = styled.div` align-items: center; padding: 6px 12px; background: ${({ theme, selected }) => - selected ? theme.primary : theme.background}; - color: ${({ theme, selected }) => (selected ? theme.white : theme.text)}; - border: 1px solid - ${({ theme, selected }) => (selected ? theme.primary : theme.border)}; + selected ? theme.transparentOrange : ''}; + border: 1px solid; + color: ${({ theme, selected }) => + selected ? theme.secondaryText : theme.text}; border-radius: 16px; cursor: pointer; transition: all 0.2s; - font-weight: ${({ selected }) => (selected ? 'bold' : 'normal')}; &:hover { background: ${({ theme }) => theme.primaryLight}; @@ -43,7 +42,9 @@ const CategorySelect = ({ categories, onChange }) => { selected={enabled} onClick={() => toggleCategory(category)} > - {category.charAt(0).toUpperCase() + category.slice(1)} + {category === 'noCategory' + ? 'No Category' + : category.charAt(0).toUpperCase() + category.slice(1)} ))}