From c98baa2be516c5c705c3144cfac17184ecdb76d8 Mon Sep 17 00:00:00 2001 From: githelsui Date: Sun, 14 Sep 2025 19:55:48 -0700 Subject: [PATCH 01/13] Section 1 - Add new `Display Data` button and update `Export` button --- .../Main/Desktop/Export/ExportButton.jsx | 2 +- .../layout/Main/Desktop/Export/useStyles.js | 12 +++++++--- .../layout/Main/Desktop/FilterMenu.jsx | 24 +++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/layout/Main/Desktop/Export/ExportButton.jsx b/src/components/layout/Main/Desktop/Export/ExportButton.jsx index b7816f5ea..22b97aae1 100644 --- a/src/components/layout/Main/Desktop/Export/ExportButton.jsx +++ b/src/components/layout/Main/Desktop/Export/ExportButton.jsx @@ -274,7 +274,7 @@ function ExportButton({ filters }) { return ( <> - + +
+
From eb32beb0f33fb4f4ae0954213ed7bd99e617a989 Mon Sep 17 00:00:00 2001 From: githelsui Date: Sun, 21 Sep 2025 21:54:47 -0700 Subject: [PATCH 02/13] Update UI for Request Types --- .../layout/Main/Desktop/StatusSelector.jsx | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/src/components/layout/Main/Desktop/StatusSelector.jsx b/src/components/layout/Main/Desktop/StatusSelector.jsx index 3c24d1c5c..e55af8e99 100644 --- a/src/components/layout/Main/Desktop/StatusSelector.jsx +++ b/src/components/layout/Main/Desktop/StatusSelector.jsx @@ -8,6 +8,11 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { updateRequestStatus } from '@reducers/filters'; import ArrowToolTip from '@components/common/ArrowToolTip'; +import Box from '@mui/material/Box'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; + const useStyles = makeStyles(() => ({ header: { marginBottom: 5, @@ -49,40 +54,57 @@ function StatusSelector({

- - updateStatusFilter('open')} - value="open" - disableRipple - > - Open - - updateStatusFilter('all')} - value="all" - disableRipple - > - All - - updateStatusFilter('closed')} - value="closed" - disableRipple - > - Closed - - + + updateStatusFilter('open')} + /> + )} + label="Open" + /> + updateStatusFilter('closed')} + /> + )} + label="Closed" + /> + + ); } From 206aed43a8b36ea5e5355a72129d458619d5cd45 Mon Sep 17 00:00:00 2001 From: githelsui Date: Sun, 21 Sep 2025 22:26:26 -0700 Subject: [PATCH 03/13] Update redux logic to handle both closed and open being selected --- .../layout/Main/Desktop/StatusSelector.jsx | 102 ++++++++++-------- src/redux/reducers/filters.js | 13 +++ 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/src/components/layout/Main/Desktop/StatusSelector.jsx b/src/components/layout/Main/Desktop/StatusSelector.jsx index e55af8e99..d277709f6 100644 --- a/src/components/layout/Main/Desktop/StatusSelector.jsx +++ b/src/components/layout/Main/Desktop/StatusSelector.jsx @@ -37,8 +37,21 @@ function StatusSelector({ const classes = useStyles(); const [selection, setSelection] = useState('open'); - const handleSelection = (event, newSelection) => { - setSelection(newSelection); + const handleSelection = (status, checked) => { + const newStatus = { + ...requestStatus, + [status]: checked, + }; + + if (newStatus.open && newStatus.closed) { + updateStatusFilter('all'); + } else if (newStatus.open) { + updateStatusFilter('open'); + } else if (newStatus.closed) { + updateStatusFilter('closed'); + } else { + updateStatusFilter('none'); + } }; return ( @@ -55,55 +68,54 @@ function StatusSelector({ - + + - - updateStatusFilter('open')} - /> - )} - label="Open" - /> - handleSelection('open', e.target.checked)} + /> + )} + label="Open" + /> + updateStatusFilter('closed')} - /> - )} - label="Closed" - /> - + style={{ + transform: 'scale(0.8)', + color: 'white', + padding: '0 0 0 20px', + }} + checked={requestStatus.closed} + onChange={e => handleSelection('closed', e.target.checked)} + /> + )} + label="Closed" + /> + ); diff --git a/src/redux/reducers/filters.js b/src/redux/reducers/filters.js index 7ff004b79..55fe14746 100644 --- a/src/redux/reducers/filters.js +++ b/src/redux/reducers/filters.js @@ -177,6 +177,19 @@ export default (state = initialState, action) => { closed: true, }, }; + case 'none': + newSearchParams.set('requestStatusOpen', false); + newSearchParams.set('requestStatusClosed', false); + url.search = newSearchParams.toString(); + window.history.replaceState(null, 'Change URL', url); + return { + ...state, + requestStatus: { + ...state.requestStatus, + open: false, + closed: false, + }, + }; // default to non-exclusive v1 'open' and 'closed' toggle code // where 'open' and 'closed' can be selected/deselected at same time From f74f64b57546e738672c51de3df48aa484a43ec0 Mon Sep 17 00:00:00 2001 From: githelsui Date: Mon, 22 Sep 2025 21:44:08 -0700 Subject: [PATCH 04/13] Update child component DateSelector, RequestTypes, StatusSelector, CouncilSelector --- src/components/DateSelector/DateSelector.jsx | 15 +++- .../Main/Desktop/CouncilSelector/index.jsx | 17 +++- .../layout/Main/Desktop/FilterMenu.jsx | 90 +++++++++++++++++-- .../layout/Main/Desktop/StatusSelector.jsx | 10 ++- .../Main/Desktop/TypeSelector/index.jsx | 4 +- 5 files changed, 116 insertions(+), 20 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index a03f098af..3654c20eb 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import moment from 'moment'; @@ -13,6 +13,7 @@ import ArrowToolTip from '@components/common/ArrowToolTip'; import options from './options'; import useStyles from './useStyles'; import DateRanges from './DateRanges'; +import { updateEndDate, updateStartDate } from '../../redux/reducers/filters'; const dateFormat = 'YYYY-MM-DD'; @@ -20,6 +21,7 @@ function DateSelector({ range, updateStartDate, updateEndDate, + hasError = false // Form Validation for blank map }) { const [expandedMenu, setExpandedMenu] = useState(false); const classes = useStyles(); @@ -44,7 +46,7 @@ function DateSelector({ return ( <> - + Date Range 
@@ -86,19 +88,26 @@ function DateSelector({ ); } +const mapStateToProps = state => ({ + updateStartDate: state.filters && state.filters.startDate, + updateEndDate: state.filters && state.filters.endDate +}); + const mapDispatchToProps = dispatch => ({ updateStartDate: date => dispatch(reduxUpdateStartDate(date)), updateEndDate: date => dispatch(reduxUpdateEndDate(date)), }); -export default connect(null, mapDispatchToProps)(DateSelector); +export default connect(mapStateToProps, mapDispatchToProps)(DateSelector); DateSelector.propTypes = { range: PropTypes.bool, updateStartDate: PropTypes.func.isRequired, updateEndDate: PropTypes.func.isRequired, + hasError: PropTypes.bool, }; DateSelector.defaultProps = { range: false, + hasError: false, }; diff --git a/src/components/layout/Main/Desktop/CouncilSelector/index.jsx b/src/components/layout/Main/Desktop/CouncilSelector/index.jsx index 6f6199d4b..d6cc0e69d 100644 --- a/src/components/layout/Main/Desktop/CouncilSelector/index.jsx +++ b/src/components/layout/Main/Desktop/CouncilSelector/index.jsx @@ -34,6 +34,7 @@ function CouncilSelector({ dispatchCloseBoundaries, resetMap, resetAddressSearch, + hasError = false // Form Validation for blank map }) { const classes = useStyles(); const [searchTerm, setSearchTerm] = useState(''); @@ -65,10 +66,10 @@ function CouncilSelector({ const newSelected = [newSelectedCouncil]; dispatchUpdateSelectedCouncils(newSelected); dispatchUpdateUnselectedCouncils(councils); - dispatchUpdateNcId(selectedCouncilId); + // dispatchUpdateNcId(selectedCouncilId); // Triggers zoom // Collapse boundaries section - dispatchCloseBoundaries(); + // dispatchCloseBoundaries(); } }; @@ -78,9 +79,14 @@ function CouncilSelector({ return ( <> - Boundaries + + Boundaries + +
- + @@ -89,6 +95,7 @@ function CouncilSelector({ )} +
); } @@ -115,6 +122,7 @@ CouncilSelector.defaultProps = { resetMap: () => {}, resetAddressSearch: () => {}, dispatchCloseBoundaries: undefined, + hasError: false, }; CouncilSelector.propTypes = { @@ -127,4 +135,5 @@ CouncilSelector.propTypes = { resetMap: PropTypes.func, resetAddressSearch: PropTypes.func, dispatchCloseBoundaries: PropTypes.func, + hasError: PropTypes.bool, }; diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index 3499ab237..3cee3534e 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -84,10 +84,56 @@ const useStyles = makeStyles(theme => ({ })); // const FilterMenu = ({ toggleMenu }) => { //toggleMenu used with GearButton -function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils, onGeocoderResult, onChangeTab, onReset, canReset }) { +function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils, onGeocoderResult, onChangeTab, onReset, canReset, + selectedCouncils, // Councils/Boundaries Form Values + startDate, endDate, // Date Range Form Values + selectedTypes, // Request Type Form Values + requestStatus, // Request Status Form Values +}) { const [expanded, setExpanded] = useState(true); const classes = useStyles(); const sharedClasses = sharedStyles(); + // Blank map implementation: form validation + const [formErrors, setFormErrors] = useState({}); + + const validateForm = () => { + let newErrors = {}; + + // Address Validation or Council Validation + if (!selectedCouncils || selectedCouncils.length === 0) { + newErrors.councils = 'Please select a Neighborhood'; + } + + // Date Range Validation + if (!startDate || !endDate) { + newErrors.dates = 'Please select both a start and end date'; + } + console.log('startDate: ' + startDate); + console.log('endDate: ' + endDate); + + // Request Type Validation + const anyTypeSelected = Object.values(selectedTypes).some(val => val); + if (!anyTypeSelected) { + newErrors.types = 'Please select at least one type of request'; + } + + // Request Status Validation + if (!requestStatus.open && !requestStatus.closed) { + newErrors.status = 'Please select at least one request status'; + } + + setFormErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleDisplayData = () => { + if (validateForm()) { + console.log("Valid form — proceeds to display data"); + // trigger data fetch + } else { + console.log("Invalid form", formErrors); + } + }; return ( @@ -135,22 +181,27 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils />
- + {formErrors.councils && ({formErrors.councils})}
- + + {formErrors.dates && ({formErrors.dates})}
- + + {formErrors.types && ({formErrors.types})}
- + + {formErrors.status && ( {formErrors.status} )}
- +
@@ -162,11 +213,24 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils ); } +// Child component props for form validation +const mapStateToProps = state => ({ + // Councils/Boundaries Form Values + selectedCouncils: state.filters.selected, + // Date Range Form Values + startDate: state.filters.startDate, + endDate: state.filters.endDate, + // Request Type Form Values + selectedTypes: state.filters.requestTypes, + // Request Status Form Values + requestStatus: state.filters.requestStatus +}); + const mapDispatchToProps = dispatch => ({ toggleMenu: () => dispatch(reduxToggleMenu()), }); -export default connect(null, mapDispatchToProps)(FilterMenu); +export default connect(mapStateToProps, mapDispatchToProps)(FilterMenu); FilterMenu.defaultProps = { resetMap: () => {}, @@ -177,7 +241,15 @@ FilterMenu.defaultProps = { onGeocoderResult: () => {}, onChangeTab: () => {}, onReset: () => {}, - canReset: false + canReset: false, + // Councils/Boundaries Form Values + selectedCouncils: null, + // Date Range Form Values + startDate: null, + endDate: null, + // Request Type Form Values + selectedTypes: {}, + // Request Status Form Values }; FilterMenu.propTypes = { @@ -190,5 +262,5 @@ FilterMenu.propTypes = { onGeocoderResult: PropTypes.func, onChangeTab: PropTypes.func, onReset: PropTypes.func, - canReset: PropTypes.bool + canReset: PropTypes.bool, }; diff --git a/src/components/layout/Main/Desktop/StatusSelector.jsx b/src/components/layout/Main/Desktop/StatusSelector.jsx index d277709f6..f48f6beed 100644 --- a/src/components/layout/Main/Desktop/StatusSelector.jsx +++ b/src/components/layout/Main/Desktop/StatusSelector.jsx @@ -12,6 +12,7 @@ import Box from '@mui/material/Box'; import FormGroup from '@mui/material/FormGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; +import { borderColor } from '@mui/system'; const useStyles = makeStyles(() => ({ header: { @@ -33,6 +34,7 @@ const useStyles = makeStyles(() => ({ function StatusSelector({ updateStatusFilter, requestStatus, + hasError = false // Form Validation for blank map }) { const classes = useStyles(); const [selection, setSelection] = useState('open'); @@ -56,7 +58,7 @@ function StatusSelector({ return ( <> -
+
Request Status 

@@ -77,9 +79,10 @@ function StatusSelector({ backgroundColor: '#192730', borderRadius: '5px', padding: '10px', - paddingTop: '3px', - paddingBottom: '3px', + paddingTop: '5px', + paddingBottom: '10px', alignItems: 'center', + border: hasError ? '1.3px solid #DE2800': undefined }} > @@ -140,4 +143,5 @@ StatusSelector.propTypes = { open: PropTypes.bool.isRequired, closed: PropTypes.bool.isRequired, }).isRequired, + hasError: PropTypes.bool, }; diff --git a/src/components/layout/Main/Desktop/TypeSelector/index.jsx b/src/components/layout/Main/Desktop/TypeSelector/index.jsx index 3c2c6cad8..55717e9a5 100644 --- a/src/components/layout/Main/Desktop/TypeSelector/index.jsx +++ b/src/components/layout/Main/Desktop/TypeSelector/index.jsx @@ -40,6 +40,7 @@ function RequestTypeSelector({ requestTypes, dispatchUpdateTypesFilter, selectedTypes, + hasError = false // Form Validation for blank map }) { const [leftCol, setLeftCol] = useState(); const [rightCol, setRightCol] = useState(); @@ -80,7 +81,7 @@ function RequestTypeSelector({ return ( - + Request Types 

@@ -101,6 +102,7 @@ function RequestTypeSelector({ padding: '5px', paddingTop: '3px', paddingBottom: '3px', + border: hasError ? '1.3px solid #DE2800': undefined }} > {/* Request Types - Left Column */} From 5fbbf6ea81ca23330dfd131d87c8d37c14e45079 Mon Sep 17 00:00:00 2001 From: githelsui Date: Mon, 22 Sep 2025 22:43:34 -0700 Subject: [PATCH 05/13] Update MapSearch for blank map --- .../layout/Main/Desktop/FilterMenu.jsx | 19 ++++++++++++++++--- src/features/Map/controls/MapSearch.jsx | 15 +++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index 3cee3534e..2f0831ea4 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -85,6 +85,7 @@ const useStyles = makeStyles(theme => ({ // const FilterMenu = ({ toggleMenu }) => { //toggleMenu used with GearButton function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils, onGeocoderResult, onChangeTab, onReset, canReset, + // Redux connected components selectedCouncils, // Councils/Boundaries Form Values startDate, endDate, // Date Range Form Values selectedTypes, // Request Type Form Values @@ -95,15 +96,23 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils const sharedClasses = sharedStyles(); // Blank map implementation: form validation const [formErrors, setFormErrors] = useState({}); + const [selectedAddress, setSelectedAddress] = useState(null); + + const handleGeocoderResult = ( result ) => { + console.log("Filter menu received address selected:", result.place_name); + setSelectedAddress(result.place_name); + }; const validateForm = () => { let newErrors = {}; // Address Validation or Council Validation - if (!selectedCouncils || selectedCouncils.length === 0) { + console.log('Selected Address: ' + selectedAddress); + if (!selectedAddress && (!selectedCouncils || selectedCouncils.length === 0)) { + newErrors.address = 'Please search by address or neighborhood'; newErrors.councils = 'Please select a Neighborhood'; } - + // Date Range Validation if (!startDate || !endDate) { newErrors.dates = 'Please select both a start and end date'; @@ -133,7 +142,9 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils } else { console.log("Invalid form", formErrors); } + setSelectedAddress(null); }; + return ( @@ -174,11 +185,13 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils map={map} geoFilterType={geoFilterType} councils={councils} - onGeocoderResult={onGeocoderResult} + onGeocoderResult={handleGeocoderResult} onChangeTab={onChangeTab} onReset={onReset} canReset={canReset} + hasError={formErrors.address ? true : false} /> + {formErrors.address && ({formErrors.address})}

{ - this.props.onGeocoderResult({ result }); + // this.props.onGeocoderResult({ result }); Temp: Do not trigger for blank map implementation + // Blank Map Implementation: pass result to FilterMenu + const selectedAddress = result.place_name; + console.log("Selected address:", selectedAddress); + this.props.onGeocoderResult(result); + // This clears the address from the Address input field. // this.geocoder.clear(); }); @@ -184,8 +189,8 @@ class MapSearch extends React.Component { ))}
*/} - Search by Address -
+ Search by Address +
); } @@ -198,6 +203,7 @@ MapSearch.propTypes = { onChangeTab: PropTypes.func, onReset: PropTypes.func, canReset: PropTypes.bool, + hasError: PropTypes.bool, }; MapSearch.defaultProps = { @@ -206,7 +212,8 @@ MapSearch.defaultProps = { onGeocoderResult: () => {}, onChangeTab: () => {}, onReset: () => {}, - canReset: false + canReset: false, + hasError: false, }; export default withStyles(styles)(MapSearch); From 3b15517f9fce8ca9d827e7198ddb22010e41dd75 Mon Sep 17 00:00:00 2001 From: githelsui Date: Tue, 23 Sep 2025 15:59:44 -0700 Subject: [PATCH 06/13] Update for export button becomes disabled until data is successfully fetched and loaded --- src/components/layout/Main/Desktop/Export/ExportButton.jsx | 7 ++++--- src/components/layout/Main/Desktop/FilterMenu.jsx | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/layout/Main/Desktop/Export/ExportButton.jsx b/src/components/layout/Main/Desktop/Export/ExportButton.jsx index 22b97aae1..5aba0344f 100644 --- a/src/components/layout/Main/Desktop/Export/ExportButton.jsx +++ b/src/components/layout/Main/Desktop/Export/ExportButton.jsx @@ -19,7 +19,7 @@ import requestTypes from '@data/requestTypes'; import useStyles from './useStyles'; import ExportDialog from './ExportDialog'; -function ExportButton({ filters }) { +function ExportButton({ filters, disabled = false }) { const { conn } = useContext(DbContext); const classes = useStyles(); const [showDialog, setShowDialog] = useState(false); @@ -274,9 +274,9 @@ function ExportButton({ filters }) { return ( <> - @@ -317,4 +317,5 @@ ExportButton.propTypes = { }), requestTypes: PropTypes.objectOf(PropTypes.bool), }).isRequired, + disabled: PropTypes.bool, }; diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index 2f0831ea4..da4392d6b 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -96,6 +96,7 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils const sharedClasses = sharedStyles(); // Blank map implementation: form validation const [formErrors, setFormErrors] = useState({}); + const [formValid, setFormValid] = useState(false); const [selectedAddress, setSelectedAddress] = useState(null); const handleGeocoderResult = ( result ) => { @@ -132,7 +133,9 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils } setFormErrors(newErrors); - return Object.keys(newErrors).length === 0; + const isValid = Object.keys(newErrors).length === 0; + setFormValid(isValid); + return isValid; }; const handleDisplayData = () => { @@ -217,7 +220,7 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils
- +
From da4e1ddaf859454c3dcc8cc524679a79da786618 Mon Sep 17 00:00:00 2001 From: githelsui Date: Tue, 23 Sep 2025 16:41:54 -0700 Subject: [PATCH 07/13] Hook up the Display Data button's on-click to Redux so that it triggers data to be downloaded --- .../layout/Main/Desktop/FilterMenu.jsx | 4 +++ src/features/Map/index.jsx | 25 ++++++++++++++----- src/redux/reducers/data.js | 23 +++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index da4392d6b..660d8e8df 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import PropTypes from 'proptypes'; import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; +import { triggerDisplayData } from '@reducers/data'; import makeStyles from '@mui/styles/makeStyles'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; @@ -98,6 +100,7 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils const [formErrors, setFormErrors] = useState({}); const [formValid, setFormValid] = useState(false); const [selectedAddress, setSelectedAddress] = useState(null); + const dispatch = useDispatch(); const handleGeocoderResult = ( result ) => { console.log("Filter menu received address selected:", result.place_name); @@ -142,6 +145,7 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils if (validateForm()) { console.log("Valid form — proceeds to display data"); // trigger data fetch + dispatch(triggerDisplayData()); } else { console.log("Invalid form", formErrors); } diff --git a/src/features/Map/index.jsx b/src/features/Map/index.jsx index 5c6549f29..86a728cfd 100644 --- a/src/features/Map/index.jsx +++ b/src/features/Map/index.jsx @@ -16,6 +16,7 @@ import { } from '@reducers/filters'; import { updateMapPosition } from '@reducers/ui'; import { trackMapExport } from '@reducers/analytics'; +import { triggerDisplayData, resetDisplayData } from '@reducers/data'; import { INTERNAL_DATE_SPEC } from '../../components/common/CONSTANTS'; import { getTypeIdFromTypeName } from '@utils'; import LoadingModal from '../../components/Loading/LoadingModal'; @@ -91,12 +92,13 @@ class MapContainer extends React.Component { async componentDidMount(props) { this.isSubscribed = true; this.processSearchParams(); + // Blank Map Implementation: createRequestTable and setData now called via Redux await this.createRequestsTable(); await this.setData(); } async componentDidUpdate(prevProps) { - const { activeMode, startDate, endDate, councilId } = this.props; + const { activeMode, startDate, endDate, councilId, shouldDisplayData } = this.props; // create conditions to check if year or startDate or endDate changed const yearChanged = moment(prevProps.startDate).year() !== moment(startDate).year(); const startDateChanged = prevProps.startDate !== startDate; @@ -106,10 +108,18 @@ class MapContainer extends React.Component { // when both the startDate and endDate are selected. const didDateRangeChange = (yearChanged || startDateChanged || endDateChanged) && endDate !== null; - if (prevProps.activeMode !== activeMode || prevProps.councilId !== councilId || didDateRangeChange) { - await this.createRequestsTable(); - await this.setData(); - } + // if (prevProps.activeMode !== activeMode || prevProps.councilId !== councilId || didDateRangeChange) { + // await this.createRequestsTable(); + // await this.setData(); + // } + + // Trigger for Display Data button + if (shouldDisplayData && shouldDisplayData !== prevProps.shouldDisplayData) { + await this.createRequestsTable(); + await this.setData(); + // reset display data flag + this.props.dispatchResetDisplayData(); + } } async componentWillUnmount() { @@ -478,7 +488,8 @@ const mapStateToProps = (state) => ({ dateRangesWithRequests: state.data.dateRangesWithRequests, isMapLoading: state.data.isMapLoading, isDbLoading: state.data.isDbLoading, - councilId: state.filters.councilId, + councilId: state.filters.councilId, + shouldDisplayData: state.data.shouldDisplayData, }); const mapDispatchToProps = (dispatch) => ({ @@ -496,6 +507,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatchUpdateEndDate: (endDate) => dispatch(updateEndDate(endDate)), dispatchUpdateNcId: (id) => dispatch(updateNcId(id)), dispatchUpdateTypesFilter: (type) => dispatch(updateRequestTypes(type)), + dispatchTriggerDisplayData: () => dispatch(triggerDisplayData()), + dispatchResetDisplayData: () => dispatch(resetDisplayData()), }); MapContainer.propTypes = {}; diff --git a/src/redux/reducers/data.js b/src/redux/reducers/data.js index 052d19aaa..8d7e7bca7 100644 --- a/src/redux/reducers/data.js +++ b/src/redux/reducers/data.js @@ -25,6 +25,8 @@ export const types = { SEND_GIT_REQUEST: 'SEND_GIT_REQUEST', GIT_RESPONSE_SUCCESS: 'GIT_RESPONSE_SUCCESS', GIT_RESPONSE_FAILURE: 'GIT_RESPONSE_FAILURE', + TRIGGER_DISPLAY_DATA: 'TRIGGER_DISPLAY_DATA', + RESET_DISPLAY_DATA: 'RESET_DISPLAY_DATA' }; export const getDbRequest = () => ({ @@ -129,6 +131,14 @@ export const gitResponseFailure = error => ({ payload: error, }); +export const triggerDisplayData = () => ({ + type: types.TRIGGER_DISPLAY_DATA +}); + +export const resetDisplayData = () => ({ + type: types.RESET_DISPLAY_DATA +}); + const initialState = { isDbLoading: true, isMapLoading: false, @@ -139,6 +149,7 @@ const initialState = { // Empty GeoJSON object. requests: { type: 'FeatureCollection', features: [] }, dateRangesWithRequests: [], + shouldDisplayData: false, }; export default (state = initialState, action) => { @@ -262,6 +273,18 @@ export default (state = initialState, action) => { selectedNcId: action.payload, }; } + case types.TRIGGER_DISPLAY_DATA: { + return { + ...state, + shouldDisplayData: true + }; + } + case types.RESET_DISPLAY_DATA: { + return { + ...state, + shouldDisplayData: false + }; + } default: return state; } From 39423647016014503712e04629a03e8cabf1ee33 Mon Sep 17 00:00:00 2001 From: githelsui Date: Fri, 3 Oct 2025 15:35:46 -0700 Subject: [PATCH 08/13] Update class for StatusSelector labels --- src/components/layout/Main/Desktop/StatusSelector.jsx | 2 -- src/features/Map/index.jsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/layout/Main/Desktop/StatusSelector.jsx b/src/components/layout/Main/Desktop/StatusSelector.jsx index f48f6beed..3fc26cec0 100644 --- a/src/components/layout/Main/Desktop/StatusSelector.jsx +++ b/src/components/layout/Main/Desktop/StatusSelector.jsx @@ -88,7 +88,6 @@ function StatusSelector({ Date: Fri, 3 Oct 2025 16:25:09 -0700 Subject: [PATCH 09/13] Prevent db request and data from being loaded on initial site load --- src/features/Map/index.jsx | 4 ++-- src/redux/reducers/data.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/Map/index.jsx b/src/features/Map/index.jsx index 8e346669b..911410d4c 100644 --- a/src/features/Map/index.jsx +++ b/src/features/Map/index.jsx @@ -93,8 +93,8 @@ class MapContainer extends React.Component { this.isSubscribed = true; this.processSearchParams(); // Blank Map Implementation: createRequestTable and setData now called via Redux - await this.createRequestsTable(); - await this.setData(); + // await this.createRequestsTable(); + // await this.setData(); } diff --git a/src/redux/reducers/data.js b/src/redux/reducers/data.js index 8d7e7bca7..9953d5c3f 100644 --- a/src/redux/reducers/data.js +++ b/src/redux/reducers/data.js @@ -140,7 +140,7 @@ export const resetDisplayData = () => ({ }); const initialState = { - isDbLoading: true, + isDbLoading: false, isMapLoading: false, error: null, pins: [], From 57ee2e3cc65fdd5cd36da50da70b857eab21d69d Mon Sep 17 00:00:00 2001 From: githelsui Date: Fri, 3 Oct 2025 16:25:29 -0700 Subject: [PATCH 10/13] Update DateSelector for blank map implementation to check form validation --- src/components/DateSelector/DateSelector.jsx | 18 ++++++++++++++---- .../common/DatePicker/DatePicker.jsx | 8 ++++++++ .../Main/Desktop/CouncilSelector/index.jsx | 2 +- src/redux/reducers/filters.js | 4 ++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 3654c20eb..ef6cd16ea 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -21,6 +21,8 @@ function DateSelector({ range, updateStartDate, updateEndDate, + startDate, + endDate, hasError = false // Form Validation for blank map }) { const [expandedMenu, setExpandedMenu] = useState(false); @@ -66,12 +68,19 @@ function DateSelector({
- setExpandedMenu(!expandedMenu)} expanded={expandedMenu}> +
+ setExpandedMenu(!expandedMenu)} expanded={expandedMenu} style={{border: hasError ? '1.3px solid #DE2800': undefined}}>
-
@@ -84,13 +93,14 @@ function DateSelector({ /> +
); } const mapStateToProps = state => ({ - updateStartDate: state.filters && state.filters.startDate, - updateEndDate: state.filters && state.filters.endDate + startDate: state.filters.startDate, + endDate: state.filters.endDate }); const mapDispatchToProps = dispatch => ({ diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 5d3dc97af..51c0a6f39 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -109,6 +109,12 @@ function DatePicker({ // This should never happen. Log a warning. console.warn('Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); } + + //Blank Map Implementation + if (startDate == null || endDate == null){ + setShowCalendar(false); + } + }, [startDate, endDate]); useOutsideClick(ref, closeCalendar); @@ -139,8 +145,10 @@ function DatePicker({ const toggleCalendar = () => { if (showCalendar) { + console.log("toggle calendar - close") closeCalendar(); } else { + console.log("toggle calendar - open") openCalendar(); } if (onTogglePresets) onTogglePresets(); diff --git a/src/components/layout/Main/Desktop/CouncilSelector/index.jsx b/src/components/layout/Main/Desktop/CouncilSelector/index.jsx index d6cc0e69d..07afab5d6 100644 --- a/src/components/layout/Main/Desktop/CouncilSelector/index.jsx +++ b/src/components/layout/Main/Desktop/CouncilSelector/index.jsx @@ -84,7 +84,7 @@ function CouncilSelector({ > Boundaries -
+
diff --git a/src/redux/reducers/filters.js b/src/redux/reducers/filters.js index 55fe14746..c58949442 100644 --- a/src/redux/reducers/filters.js +++ b/src/redux/reducers/filters.js @@ -51,8 +51,8 @@ export const updateRequestStatus = status => ({ const initialState = { // dateRange: null, // Always store dates using the INTERNAL_DATE_SPEC. - startDate: moment(DATE_RANGES[0].startDate, USER_DATE_SPEC).format(INTERNAL_DATE_SPEC), - endDate: moment(DATE_RANGES[0].endDate, USER_DATE_SPEC).format(INTERNAL_DATE_SPEC), + startDate: null, + endDate: null, councilId: null, selected: [], unselected: [], From e85e8d5523da3c722285e5598d1a7228b6e0d2e0 Mon Sep 17 00:00:00 2001 From: githelsui Date: Tue, 14 Oct 2025 21:35:28 -0700 Subject: [PATCH 11/13] Update DatePicker to account for initial null values and further date selection via ReactDayPicker --- .../common/ReactDayPicker/ReactDayPicker.jsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/common/ReactDayPicker/ReactDayPicker.jsx b/src/components/common/ReactDayPicker/ReactDayPicker.jsx index b1569fcd8..63fe6548e 100644 --- a/src/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/src/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -157,6 +157,7 @@ function ReactDayPicker({ // enteredTo represents the day that the user is currently hovering over. const [enteredTo, setEnteredTo] = useState(endDate); + // Sets start date const setFromDay = day => { updateStartDate(moment(day).format(INTERNAL_DATE_SPEC)); }; @@ -166,13 +167,15 @@ function ReactDayPicker({ }; const handleDayClick = day => { - if (!range) { - setFromDay(day); - return; - } - - // If both startDate and endDate were already selected. Start a new range selection. - if (startDate && endDate){ + //Blank Map Implementation + console.warn('Date selected: ', day); + + // Initial state: null startDate and endDate, user first selects start date + if(!startDate){ + console.warn('Start Date Selected: ', day); + setFromDay(day); + } // If both startDate and endDate were already selected. Start a new range selection. + else if (startDate && endDate){ setFromDay(day); updateEndDate(null); setEnteredTo(null); @@ -191,7 +194,7 @@ function ReactDayPicker({ } } else { // This should never happen. Log a warning. - console.warn('Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); + console.warn('ReactDayPicker: Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); } }; From 9e36469077f106ff2d95f81646b1169dc12f7f67 Mon Sep 17 00:00:00 2001 From: githelsui Date: Wed, 15 Oct 2025 18:17:05 -0700 Subject: [PATCH 12/13] Update ReactDayPicker to account for missing start date validation --- src/components/common/DatePicker/DatePicker.jsx | 13 +++++++------ .../common/ReactDayPicker/ReactDayPicker.jsx | 15 +++++++++++++-- src/components/layout/Main/Desktop/FilterMenu.jsx | 8 ++++++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 51c0a6f39..19416dc2e 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -99,12 +99,13 @@ function DatePicker({ () => { if (startDate && endDate){ setShowCalendar(false); - } else if (startDate && !endDate){ - // The calendar was closed with an incomplete date range selection so we need to restart - // startDate and endDate to their initial values - updateStartDate(initialStartDate); - updateEndDate(initialEndDate); - setShowCalendar(false); + // For Blank Map Implementation: no need to reset date range selection for testing purposes for date form validation + // } else if (startDate && !endDate){ + // // The calendar was closed with an incomplete date range selection so we need to restart + // // startDate and endDate to their initial values + // updateStartDate(initialStartDate); + // updateEndDate(initialEndDate); + // setShowCalendar(false); } else { // This should never happen. Log a warning. console.warn('Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); diff --git a/src/components/common/ReactDayPicker/ReactDayPicker.jsx b/src/components/common/ReactDayPicker/ReactDayPicker.jsx index 63fe6548e..fbf856680 100644 --- a/src/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/src/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -168,12 +168,23 @@ function ReactDayPicker({ const handleDayClick = day => { //Blank Map Implementation - console.warn('Date selected: ', day); - + console.warn('Current start date: ', startDate); + console.warn('Current end date: ', endDate); + var selectedDate = moment(day).format(INTERNAL_DATE_SPEC) + console.warn('Date selected: ', selectedDate); + // Initial state: null startDate and endDate, user first selects start date if(!startDate){ console.warn('Start Date Selected: ', day); setFromDay(day); + // Case: missing start date, user unselects start date + } else if(startDate && startDate == selectedDate) { + console.warn('Same start date selected, remove start date: ', selectedDate); + updateStartDate(null); + // Case: missing end date, user unselects end date + } else if(endDate && endDate == selectedDate) { + console.warn('Same end date selected, remove end date: ', selectedDate); + updateEndDate(null); } // If both startDate and endDate were already selected. Start a new range selection. else if (startDate && endDate){ setFromDay(day); diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index 660d8e8df..8fd870089 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -118,8 +118,12 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils } // Date Range Validation - if (!startDate || !endDate) { - newErrors.dates = 'Please select both a start and end date'; + if (!startDate && !endDate) { + newErrors.dates = 'Please select a date range'; + } else if (!startDate && endDate) { + newErrors.dates = 'Please select a start date'; + } else if (startDate && !endDate) { + newErrors.dates = 'Please select an end date'; } console.log('startDate: ' + startDate); console.log('endDate: ' + endDate); From ad21c8f375fb291c10ad3cbeb4a54303d22638f3 Mon Sep 17 00:00:00 2001 From: githelsui Date: Tue, 18 Nov 2025 16:00:30 -0800 Subject: [PATCH 13/13] Add event listener for cleared search bar to update form validation --- src/components/layout/Main/Desktop/FilterMenu.jsx | 10 +++++++--- src/features/Map/controls/MapSearch.jsx | 11 +++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/layout/Main/Desktop/FilterMenu.jsx b/src/components/layout/Main/Desktop/FilterMenu.jsx index 8fd870089..9395d8bdd 100644 --- a/src/components/layout/Main/Desktop/FilterMenu.jsx +++ b/src/components/layout/Main/Desktop/FilterMenu.jsx @@ -103,8 +103,12 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils const dispatch = useDispatch(); const handleGeocoderResult = ( result ) => { - console.log("Filter menu received address selected:", result.place_name); - setSelectedAddress(result.place_name); + console.log("Filter menu - received address selected from MapSearch:", result.place_name); + if(result.place_name == '') { + setSelectedAddress(null); + } else { + setSelectedAddress(result.place_name); + } }; const validateForm = () => { @@ -153,7 +157,7 @@ function FilterMenu({ resetMap, resetAddressSearch, map, geoFilterType, councils } else { console.log("Invalid form", formErrors); } - setSelectedAddress(null); + // setSelectedAddress(null); }; diff --git a/src/features/Map/controls/MapSearch.jsx b/src/features/Map/controls/MapSearch.jsx index dcbd04e79..b3b4bccf7 100644 --- a/src/features/Map/controls/MapSearch.jsx +++ b/src/features/Map/controls/MapSearch.jsx @@ -133,7 +133,7 @@ class MapSearch extends React.Component { // Blank Map Implementation: pass result to FilterMenu const selectedAddress = result.place_name; - console.log("Selected address:", selectedAddress); + console.log("MapSearch - Selected address:", selectedAddress); this.props.onGeocoderResult(result); // This clears the address from the Address input field. @@ -145,6 +145,13 @@ class MapSearch extends React.Component { // Add a custom event listener to clear the Address Search Input field this.addListener(geocoderElement, settings.map.eventName.reset, ()=>this.geocoder.clear() ) + + //Listens to event when search address bar is cleared + this.geocoder.on('clear', (result) => { + console.log("Clear event triggered"); + //Updates props.onGeocoderResult with an empty string and handles empty string value in FilterMenu for Blank Map Form Validation + this.props.onGeocoderResult({place_name: ''}); + }); // this.setTab(GEO_FILTER_TYPES.address); } @@ -157,7 +164,7 @@ class MapSearch extends React.Component { setTab = tab => { this.props.onChangeTab(tab); - this.geocoder.clear(); + // this.geocoder.clear(); this.geocoder.setPlaceholder(`Enter ${tab.toLowerCase()}`); switch(tab) { case GEO_FILTER_TYPES.address: