From 8b63d0e0b6ba9608370106cb7e972b9f3aaab665 Mon Sep 17 00:00:00 2001 From: Xinyi Date: Tue, 9 Dec 2025 17:50:51 -0500 Subject: [PATCH 01/12] fix: project and tool filters --- .../ItemList/ItemListView.module.css | 13 +- .../BMDashboard/ItemList/SelectForm.jsx | 78 +++++++----- .../BMDashboard/ItemList/SelectItem.jsx | 117 +++++++++++------- .../ToolItemList/ToolItemListView.jsx | 56 ++++++--- 4 files changed, 168 insertions(+), 96 deletions(-) diff --git a/src/components/BMDashboard/ItemList/ItemListView.module.css b/src/components/BMDashboard/ItemList/ItemListView.module.css index 21335b697f..d885a1b9b1 100644 --- a/src/components/BMDashboard/ItemList/ItemListView.module.css +++ b/src/components/BMDashboard/ItemList/ItemListView.module.css @@ -41,10 +41,11 @@ .selectInput { display: grid; - grid-template-columns: auto 1fr auto 1fr auto 1fr; + grid-template-columns: auto 1fr auto 1fr; align-items: center; gap: 15px; width: 100%; + min-width: 400px; max-width: 1200px; margin: 0 auto 10px auto; overflow: visible; @@ -62,14 +63,20 @@ .selectInput select { height: 38px; width: 100%; - min-width: 220px; - max-width: 240px; + min-width: 400px; + max-width: 1200px; padding: 5px; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 8px; } +.selectInput :global(.react-select__control) { + width: 100%; + max-width: 1200px; + min-width: 400px; +} + .selectInput input[type='text'] { padding: 5px; margin-bottom: 8px; diff --git a/src/components/BMDashboard/ItemList/SelectForm.jsx b/src/components/BMDashboard/ItemList/SelectForm.jsx index 27f62b6848..226c47bc60 100644 --- a/src/components/BMDashboard/ItemList/SelectForm.jsx +++ b/src/components/BMDashboard/ItemList/SelectForm.jsx @@ -1,43 +1,61 @@ -import { Form, FormGroup, Label, Input } from 'reactstrap'; +import { useEffect, useMemo, useState } from 'react'; +import { Form, FormGroup, Label } from 'reactstrap'; +import Select from 'react-select'; import styles from './ItemListView.module.css'; +const PROJECT_KEY = 'tool_selected_projects'; + export default function SelectForm({ items, setSelectedProject, setSelectedItem }) { - let projectsSet = []; - if (items.length) { - projectsSet = [...new Set(items.map(el => el.project?.name))]; - } + const [selectedProjects, setSelectedProjects] = useState([]); + + // Build project list + const projectOptions = useMemo(() => { + if (!items?.length) return []; + const unique = [...new Set(items.map(i => i.project?.name).filter(Boolean))]; + return unique.map(name => ({ + label: name, + value: name, + })); + }, [items]); + + // Restore saved values + useEffect(() => { + const saved = JSON.parse(localStorage.getItem(PROJECT_KEY)); + + if (Array.isArray(saved)) { + setSelectedProjects(saved); + setSelectedProject(saved.map(p => p.value)); + } + }, []); - const handleChange = event => { + const handleChange = selected => { + const values = selected || []; + + setSelectedProjects(values); setSelectedItem('all'); - setSelectedProject(event.target.value); + setSelectedProject(values.length ? values.map(v => v.value) : 'all'); + + localStorage.setItem(PROJECT_KEY, JSON.stringify(values)); }; return (
- - - + + + setSelectedItem(e.target.value)} - disabled={!items.length} - > - {items.length ? ( - <> - - {itemSet.map(itemName => ( - - ))} - - ) : ( - - )} - + + - Select one or more projects to filter results.
); } + +SelectForm.propTypes = { + items: PropTypes.array.isRequired, + setSelectedProject: PropTypes.func.isRequired, + setSelectedItem: PropTypes.func.isRequired, +}; \ No newline at end of file diff --git a/src/components/BMDashboard/ItemList/SelectItem.jsx b/src/components/BMDashboard/ItemList/SelectItem.jsx index 4ff303f428..3dab41860c 100644 --- a/src/components/BMDashboard/ItemList/SelectItem.jsx +++ b/src/components/BMDashboard/ItemList/SelectItem.jsx @@ -1,34 +1,25 @@ import { useEffect, useMemo, useState } from 'react'; import { Form, FormGroup, Label } from 'reactstrap'; import Select from 'react-select'; +import PropTypes from 'prop-types'; import styles from './ItemListView.module.css'; const ITEM_KEY = 'tool_selected_items'; -export default function SelectItem({ - items, - selectedProject, - selectedItem, - setSelectedItem, - label, -}) { +export default function SelectItem({ items, selectedProject, selectedItem, setSelectedItem, label }) { const [localValues, setLocalValues] = useState([]); - // ✅ Build filtered tool options + // Build contextually aware tool options based on the active project selections const itemOptions = useMemo(() => { if (!items?.length) return []; let list = items; - - if (Array.isArray(selectedProject)) { - list = items.filter(i => selectedProject.includes(i.project?.name) && i.itemType?.name); - } else if (selectedProject !== 'all') { - list = items.filter(i => i.project?.name === selectedProject && i.itemType?.name); - } else { - list = items.filter(i => i.itemType?.name); + if (Array.isArray(selectedProject) && selectedProject.length > 0) { + list = items.filter(i => selectedProject.includes(i.project?.name)); } - const names = [...new Set(list.map(i => i.itemType.name))]; + // Fixed: Added optional chaining safety wrapper + const names = [...new Set(list.map(i => i.itemType?.name).filter(Boolean))]; return names.map(name => ({ label: name, @@ -36,31 +27,45 @@ export default function SelectItem({ })); }, [items, selectedProject]); - // ✅ Restore saved selections + // Restore cached selections from persistent state safely useEffect(() => { - const saved = JSON.parse(localStorage.getItem(ITEM_KEY)); - - if (Array.isArray(saved)) { - setLocalValues(saved); - setSelectedItem(saved.map(s => s.value)); + try { + const saved = JSON.parse(localStorage.getItem(ITEM_KEY)); + if (Array.isArray(saved)) { + setLocalValues(saved); + setSelectedItem(saved.map(s => s.value)); + } + } catch (error) { + console.error('Failed to parse cached item filter scope:', error); } - }, []); + }, [setSelectedItem]); - // ✅ Sync reset from parent + // Catch reset triggers fired from parent views/companion components useEffect(() => { - const isMulti = Array.isArray(selectedItem); - - if (selectedItem === 'all' || (isMulti && selectedItem.length === 0)) { + if (Array.isArray(selectedItem) && selectedItem.length === 0) { setLocalValues([]); } }, [selectedItem]); + // Auto-prune active selection tags if they fall out of scope when projects shift + useEffect(() => { + if (localValues.length > 0 && itemOptions.length > 0) { + const activeKeys = itemOptions.map(opt => opt.value); + const alignedValues = localValues.filter(val => activeKeys.includes(val.value)); + + if (alignedValues.length !== localValues.length) { + setLocalValues(alignedValues); + setSelectedItem(alignedValues.map(v => v.value)); + localStorage.setItem(ITEM_KEY, JSON.stringify(alignedValues)); + } + } + }, [itemOptions, localValues, setSelectedItem]); + const handleChange = selected => { const values = selected || []; setLocalValues(values); - setSelectedItem(values.length ? values.map(v => v.value) : 'all'); - + setSelectedItem(values.map(v => v.value)); localStorage.setItem(ITEM_KEY, JSON.stringify(values)); }; @@ -70,7 +75,6 @@ export default function SelectItem({ - - Select one or more projects to filter results. ); @@ -70,4 +145,4 @@ SelectForm.propTypes = { items: PropTypes.array.isRequired, setSelectedProject: PropTypes.func.isRequired, setSelectedItem: PropTypes.func.isRequired, -}; \ No newline at end of file +}; diff --git a/src/components/BMDashboard/ItemList/SelectItem.jsx b/src/components/BMDashboard/ItemList/SelectItem.jsx index 3dab41860c..09c7f592fd 100644 --- a/src/components/BMDashboard/ItemList/SelectItem.jsx +++ b/src/components/BMDashboard/ItemList/SelectItem.jsx @@ -3,10 +3,17 @@ import { Form, FormGroup, Label } from 'reactstrap'; import Select from 'react-select'; import PropTypes from 'prop-types'; import styles from './ItemListView.module.css'; - +import { useSelector } from 'react-redux'; const ITEM_KEY = 'tool_selected_items'; -export default function SelectItem({ items, selectedProject, selectedItem, setSelectedItem, label }) { +export default function SelectItem({ + items, + selectedProject, + selectedItem, + setSelectedItem, + label, +}) { + const darkMode = useSelector(state => state.theme?.darkMode || false); const [localValues, setLocalValues] = useState([]); // Build contextually aware tool options based on the active project selections @@ -52,7 +59,7 @@ export default function SelectItem({ items, selectedProject, selectedItem, setSe if (localValues.length > 0 && itemOptions.length > 0) { const activeKeys = itemOptions.map(opt => opt.value); const alignedValues = localValues.filter(val => activeKeys.includes(val.value)); - + if (alignedValues.length !== localValues.length) { setLocalValues(alignedValues); setSelectedItem(alignedValues.map(v => v.value)); @@ -63,18 +70,92 @@ export default function SelectItem({ items, selectedProject, selectedItem, setSe const handleChange = selected => { const values = selected || []; - setLocalValues(values); setSelectedItem(values.map(v => v.value)); localStorage.setItem(ITEM_KEY, JSON.stringify(values)); }; + const selectStyles = { + control: (base, state) => ({ + ...base, + backgroundColor: darkMode ? '#2a3f5f' : base.backgroundColor, + borderColor: darkMode ? '#3a506b' : base.borderColor, + color: darkMode ? '#e0e0e0' : base.color, + boxShadow: state.isFocused + ? darkMode + ? '0 0 0 1px #6af1ea' + : base.boxShadow + : base.boxShadow, + '&:hover': { + borderColor: darkMode + ? '#5a7a9b' + : base['&:hover'] + ? base['&:hover'].borderColor + : base.borderColor, + }, + }), + menu: base => ({ + ...base, + backgroundColor: darkMode ? '#1c2541' : base.backgroundColor, + border: darkMode ? '1px solid #3a506b' : base.border, + }), + option: (base, state) => { + let backgroundColor = base.backgroundColor; + if (state.isSelected) { + backgroundColor = darkMode ? '#3a506b' : base.backgroundColor; + } else if (state.isFocused) { + backgroundColor = darkMode ? '#2a3f5f' : base.backgroundColor; + } else { + backgroundColor = darkMode ? '#1c2541' : base.backgroundColor; + } + + return { + ...base, + backgroundColor, + color: darkMode ? '#e0e0e0' : base.color, + '&:hover': { + backgroundColor: darkMode + ? '#3a506b' + : base['&:hover'] + ? base['&:hover'].backgroundColor + : base.backgroundColor, + }, + }; + }, + multiValue: base => ({ + ...base, + backgroundColor: darkMode ? '#3a506b' : base.backgroundColor, + }), + multiValueLabel: base => ({ + ...base, + color: darkMode ? '#ffffff' : base.color, + }), + multiValueRemove: base => ({ + ...base, + color: darkMode ? '#ffffff' : base.color, + '&:hover': { + backgroundColor: darkMode ? '#5a7a9b' : '#e9ecef', + color: darkMode ? '#ffffff' : '#495057', + }, + }), + placeholder: base => ({ + ...base, + color: darkMode ? '#b5bac5' : base.color, + }), + singleValue: base => ({ + ...base, + color: darkMode ? '#e0e0e0' : base.color, + }), + input: base => ({ + ...base, + color: darkMode ? '#e0e0e0' : base.color, + }), + }; + return ( -
+ e.preventDefault()}> - +