From 54c6dfe20bfbcbc553183882876774f8b63a5c32 Mon Sep 17 00:00:00 2001 From: theflippedbit <7685578+theflippedbit@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:46:07 -0400 Subject: [PATCH] WV-4672 ManageMyCandidates: Clicking on a Candidate card opens side drawer --- src/js/common/components/CardForListBody.jsx | 22 ++++- src/js/common/stores/PoliticianStore.js | 17 +++- .../CandidateCardForList.jsx | 15 +++ .../PoliticianCardForList.jsx | 25 +++++ .../VoterGuide/OrganizationModal.jsx | 95 ++++++++++++++----- src/js/pages/Campaigns/CampaignsHome.jsx | 6 +- 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/src/js/common/components/CardForListBody.jsx b/src/js/common/components/CardForListBody.jsx index a4258136d..2998eafd0 100644 --- a/src/js/common/components/CardForListBody.jsx +++ b/src/js/common/components/CardForListBody.jsx @@ -37,7 +37,7 @@ function CardForListBody (props) { ballotItemDisplayName, candidateWeVoteId, classes, districtName, finalElectionDateInPast, hideCardMargins, hideItemActionBar, isClaimedProfile, limitCardWidth, linkedCampaignXWeVoteId, officeName, - photoLargeUrl, politicalParty, politicianBasePath, + onDisplayNameClick, photoLargeUrl, politicalParty, politicianBasePath, politicianDescription, politicianWeVoteId, profileImageBackgroundColor, searchText, showPoliticianOpenInNewWindow, stateCode, supportPolitician, tagIdBaseName, @@ -129,7 +129,16 @@ function CardForListBody (props) { className={isCordova() ? 'u-link-color u-link-underline' : ''} id={`${tagIdBaseName}DisplayName`} to={politicianBasePath} - onClick={() => (isCordova() ? AppObservableStore.setShowOrganizationModal(false) : null)} + onClick={(e) => { + if (onDisplayNameClick) { + e.preventDefault(); + onDisplayNameClick(); + return; + } + if (isCordova()) { + AppObservableStore.setShowOrganizationModal(false); + } + }} > {highlightSearchText(ballotItemDisplayName || nameFromUrl, searchText)} @@ -421,7 +430,13 @@ function CardForListBody (props) { hideCardMargins={hideCardMargins} id={`${tagIdBaseName}PhotoDesktop`} limitCardWidth={limitCardWidth} - onClick={hideCardMargins ? null : () => historyPush(politicianBasePath)} + onClick={hideCardMargins ? null : () => { + if (onDisplayNameClick) { + onDisplayNameClick(); + return; + } + historyPush(politicianBasePath); + }} profileImageBackgroundColor={profileImageBackgroundColor} useVerticalCard={useVerticalCard} > @@ -504,6 +519,7 @@ CardForListBody.propTypes = { limitCardWidth: PropTypes.bool, linkedCampaignXWeVoteId: PropTypes.string, officeName: PropTypes.string, + onDisplayNameClick: PropTypes.func, photoLargeUrl: PropTypes.string, politicalParty: PropTypes.string, politicianBasePath: PropTypes.string.isRequired, diff --git a/src/js/common/stores/PoliticianStore.js b/src/js/common/stores/PoliticianStore.js index 8f05472e5..31e819c55 100644 --- a/src/js/common/stores/PoliticianStore.js +++ b/src/js/common/stores/PoliticianStore.js @@ -27,6 +27,7 @@ class PoliticianStore extends ReduceStore { politicianEmailQueuedToSaveSet: false, politicianNameQueuedToSave: '', politicianNameQueuedToSaveSet: false, + politicianQueryResultsWeVoteIds: [], // politician_we_vote_id's returned by the most recent politiciansQuery/politiciansRetrieve, as opposed to ones individually cached via politicianRetrieve politicianPhotoQueuedToSave: '', politicianPhotoQueuedToSaveSet: false, politicianPhotoTooBig: false, @@ -90,6 +91,16 @@ class PoliticianStore extends ReduceStore { return politicianList || []; } + // Unlike getPoliticianList (which returns every politician ever cached, including ones pulled in one-at-a-time by + // politicianRetrieve, e.g. when a candidate's side drawer is opened), this only returns the politicians returned by + // the most recent politiciansQuery/politiciansRetrieve call (e.g. "top politicians for this state"). + getPoliticianQueryResultsList () { + const { allCachedPoliticians, politicianQueryResultsWeVoteIds } = this.getState(); + return politicianQueryResultsWeVoteIds + .map((politicianWeVoteId) => allCachedPoliticians[politicianWeVoteId]) + .filter((politician) => !!politician); + } + getPoliticianName (politicianWeVoteId) { const politician = this.getState().allCachedPoliticians[politicianWeVoteId] || {}; if (politician) { @@ -404,7 +415,7 @@ class PoliticianStore extends ReduceStore { } = state; let { allCachedPoliticians, allCachedPoliticianOwners, allCachedCandidateListsByPolitician, allCachedPoliticianOwnerPhotos, - politicianListsByOfficeWeVoteId, + politicianListsByOfficeWeVoteId, politicianQueryResultsWeVoteIds, voterCanSendUpdatesPoliticianWeVoteIds, voterCanVoteForPoliticianWeVoteIds, voterOwnedPoliticianWeVoteIds, // voterStartedPoliticianWeVoteIds, } = state; @@ -635,6 +646,7 @@ class PoliticianStore extends ReduceStore { politicianListsByOfficeWeVoteId = {}; } localPoliticianList = []; + politicianQueryResultsWeVoteIds = []; politicianList.forEach((one) => { if (action.type === 'politiciansQuery') { // Since the politiciansQuery doesn't return all of the @@ -643,8 +655,10 @@ class PoliticianStore extends ReduceStore { if (!(one.politician_we_vote_id in allCachedPoliticians)) { allCachedPoliticians[one.politician_we_vote_id] = one; } + politicianQueryResultsWeVoteIds.push(one.politician_we_vote_id); } else { allCachedPoliticians[one.we_vote_id] = one; + politicianQueryResultsWeVoteIds.push(one.we_vote_id); } localPoliticianList.push(one); }); @@ -653,6 +667,7 @@ class PoliticianStore extends ReduceStore { ...state, allCachedPoliticians, politicianListsByOfficeWeVoteId, + politicianQueryResultsWeVoteIds, }; case 'voterCanEditPolitician': diff --git a/src/js/components/CandidateListRoot/CandidateCardForList.jsx b/src/js/components/CandidateListRoot/CandidateCardForList.jsx index cb38fd2b8..e35d6dbe3 100644 --- a/src/js/components/CandidateListRoot/CandidateCardForList.jsx +++ b/src/js/components/CandidateListRoot/CandidateCardForList.jsx @@ -3,6 +3,7 @@ import React, { Component, Suspense } from 'react'; import CardForListBodyPlaceholder from '../../common/components/CardForListBodyPlaceholder'; import { getTodayAsInteger } from '../../common/utils/dateFormat'; import { renderLog } from '../../common/utils/logging'; +import AppObservableStore from '../../common/stores/AppObservableStore'; import CampaignSupporterStore from '../../common/stores/CampaignSupporterStore'; import CandidateStore from '../../stores/CandidateStore'; import keepHelpingDestination from '../../common/utils/keepHelpingDestination'; @@ -21,6 +22,7 @@ class CandidateCardForList extends Component { this.getCampaignXBasePath = this.getCampaignXBasePath.bind(this); this.getPathToUseToKeepHelping = this.getPathToUseToKeepHelping.bind(this); this.getPoliticianBasePath = this.getPoliticianBasePath.bind(this); + this.onCandidateNameClick = this.onCandidateNameClick.bind(this); // this.pullCampaignXSupporterVoterEntry = this.pullCampaignXSupporterVoterEntry.bind(this); } @@ -80,6 +82,18 @@ class CandidateCardForList extends Component { }); } + onCandidateNameClick () { + const { candidate } = this.state; + const candidateWeVoteId = candidate && candidate.we_vote_id; + if (!candidateWeVoteId) { + return; + } + AppObservableStore.setOrganizationModalBallotItemWeVoteId(candidateWeVoteId); + AppObservableStore.setHideOrganizationModalBallotItemInfo(false); + AppObservableStore.setHideOrganizationModalPositions(false); + AppObservableStore.setShowOrganizationModal(true); + } + getCampaignXBasePath () { const { candidate } = this.state; // console.log('candidate:', candidate); @@ -232,6 +246,7 @@ class CandidateCardForList extends Component { limitCardWidth={limitCardWidth} linkedCampaignXWeVoteId={linkedCampaignXWeVoteId} officeName={contestOfficeName} + onDisplayNameClick={candidateWeVoteId ? this.onCandidateNameClick : undefined} pathToUseToKeepHelping={pathToUseToKeepHelping} photoLargeUrl={candidatePhotoLargeUrl} politicalParty={politicalParty.length ? politicalParty : ''} diff --git a/src/js/components/PoliticianListRoot/PoliticianCardForList.jsx b/src/js/components/PoliticianListRoot/PoliticianCardForList.jsx index 7b984334e..1c5bf533e 100644 --- a/src/js/components/PoliticianListRoot/PoliticianCardForList.jsx +++ b/src/js/components/PoliticianListRoot/PoliticianCardForList.jsx @@ -3,8 +3,10 @@ import React, { Component, Suspense } from 'react'; import CardForListBodySkeleton from '../../common/components/CardForListBodySkeleton'; import { getTodayAsInteger } from '../../common/utils/dateFormat'; import { renderLog } from '../../common/utils/logging'; +import AppObservableStore from '../../common/stores/AppObservableStore'; import CampaignSupporterStore from '../../common/stores/CampaignSupporterStore'; import CandidateStore from '../../stores/CandidateStore'; +import PoliticianActions from '../../common/actions/PoliticianActions'; import PoliticianStore from '../../common/stores/PoliticianStore'; import keepHelpingDestination from '../../common/utils/keepHelpingDestination'; import { mostLikelyCandidateDictFromList } from '../../utils/candidateFunctions'; @@ -24,6 +26,7 @@ class PoliticianCardForList extends Component { this.getCampaignXBasePath = this.getCampaignXBasePath.bind(this); this.getPathToUseToKeepHelping = this.getPathToUseToKeepHelping.bind(this); this.getPoliticianBasePath = this.getPoliticianBasePath.bind(this); + this.onPoliticianNameClick = this.onPoliticianNameClick.bind(this); // this.pullCampaignXSupporterVoterEntry = this.pullCampaignXSupporterVoterEntry.bind(this); } @@ -90,6 +93,12 @@ class PoliticianCardForList extends Component { const { linked_campaignx_we_vote_id: linkedCampaignXWeVoteId, } = politician; + // The politician list-view (politiciansQuery/politiciansRetrieve) doesn't include candidate_list, so we need a + // one-time full retrieve to learn which candidate record (if any) to open in the side drawer on name click. + if (!('candidate_list' in politician) && !this.fullPoliticianRetrieveTriggered) { + this.fullPoliticianRetrieveTriggered = true; + PoliticianActions.politicianRetrieve(politicianWeVoteId); + } if (politician.candidate_list && politician.candidate_list.length > 0) { const mostLikelyCandidate = mostLikelyCandidateDictFromList(politician.candidate_list); // console.log('mostLikelyCandidate: ', mostLikelyCandidate); @@ -106,6 +115,21 @@ class PoliticianCardForList extends Component { }); } + onPoliticianNameClick () { + const { politicianWeVoteId } = this.props; + const { candidateWeVoteId } = this.state; + // Prefer the politician's current candidate record when known; otherwise fall back to the politician + // we_vote_id itself so the drawer always opens instead of navigating to the full page. + const ballotItemWeVoteId = candidateWeVoteId || politicianWeVoteId; + if (!ballotItemWeVoteId) { + return; + } + AppObservableStore.setOrganizationModalBallotItemWeVoteId(ballotItemWeVoteId); + AppObservableStore.setHideOrganizationModalBallotItemInfo(false); + AppObservableStore.setHideOrganizationModalPositions(false); + AppObservableStore.setShowOrganizationModal(true); + } + getCampaignXBasePath () { const { politician } = this.state; // console.log('politician:', politician); @@ -262,6 +286,7 @@ class PoliticianCardForList extends Component { limitCardWidth={limitCardWidth} linkedCampaignXWeVoteId={linkedCampaignXWeVoteId} officeName={contestOfficeName} + onDisplayNameClick={this.onPoliticianNameClick} pathToUseToKeepHelping={pathToUseToKeepHelping} photoLargeUrl={politicianPhotoLargeUrl} politicalParty={politicalParty} diff --git a/src/js/components/VoterGuide/OrganizationModal.jsx b/src/js/components/VoterGuide/OrganizationModal.jsx index 773fd5d4b..95a30d0e3 100644 --- a/src/js/components/VoterGuide/OrganizationModal.jsx +++ b/src/js/components/VoterGuide/OrganizationModal.jsx @@ -27,6 +27,7 @@ import MeasureStore from '../../stores/MeasureStore'; import VoterGuideStore from '../../stores/VoterGuideStore'; import VoterStore from '../../stores/VoterStore'; import CardForListBodySkeleton from '../../common/components/CardForListBodySkeleton'; +import PoliticianStore from '../../common/stores/PoliticianStore'; import VoterPositionEntryAndDisplay from '../PositionItem/VoterPositionEntryAndDisplay'; import { Candidate, CandidateNameAndPartyWrapper, CandidateNameH4, CandidateParty, CandidateTopRow } from '../Style/BallotStyles'; import { DrawerHeaderAnimateDownInnerContainer, DrawerHeaderAnimateDownOuterContainer } from '../Style/drawerLayoutStyles'; @@ -79,13 +80,17 @@ class OrganizationModal extends Component { this.appStateSubscription = messageService.getMessage().subscribe(() => this.onAppObservableStoreChange()); this.candidateStoreListener = CandidateStore.addListener(this.onCandidateStoreChange.bind(this)); this.measureStoreListener = MeasureStore.addListener(this.onMeasureStoreChange.bind(this)); + this.politicianStoreListener = PoliticianStore.addListener(this.onPoliticianStoreChange.bind(this)); const { ballotItemWeVoteId } = this.props; // console.log('ballotItemWeVoteId:', ballotItemWeVoteId); const isMeasure = stringContains('meas', ballotItemWeVoteId); const isCandidate = stringContains('cand', ballotItemWeVoteId); + // A politician we_vote_id can be passed directly when a politician has no linked candidate record to show instead + const isPolitician = !isCandidate && !isMeasure; this.setState({ isCandidate, isMeasure, + isPolitician, }); setTimeout(() => { const drawer = document.querySelector('.MuiDrawer-paper'); @@ -172,6 +177,23 @@ class OrganizationModal extends Component { }); AnalyticsActions.saveActionMeasure(VoterStore.electionId(), ballotItemWeVoteId); } + if (isPolitician) { + PoliticianActions.politicianRetrieve(ballotItemWeVoteId); + const politician = PoliticianStore.getPoliticianByWeVoteId(ballotItemWeVoteId); + const { + linked_campaignx_we_vote_id: linkedCampaignXWeVoteId, + political_party: politicalParty, + politician_name: ballotItemDisplayName, + we_vote_hosted_profile_image_url_large: politicianImageUrlLarge, + } = politician; + this.setState({ + ballotItemDisplayName, + linkedCampaignXWeVoteId, + politicalParty, + politicianImageUrlLarge, + politicianWeVoteId: ballotItemWeVoteId, + }); + } if (apiCalming('organizationsFollowedRetrieve', 60000)) { OrganizationActions.organizationsFollowedRetrieve(); } @@ -205,6 +227,7 @@ class OrganizationModal extends Component { componentWillUnmount () { this.candidateStoreListener.remove(); this.measureStoreListener.remove(); + this.politicianStoreListener.remove(); this.appStateSubscription.unsubscribe(); AppObservableStore.setScrolledDownDrawer(false); } @@ -293,6 +316,26 @@ class OrganizationModal extends Component { } } + onPoliticianStoreChange () { + const { ballotItemWeVoteId } = this.props; + const { isPolitician } = this.state; + if (isPolitician) { + const politician = PoliticianStore.getPoliticianByWeVoteId(ballotItemWeVoteId); + const { + linked_campaignx_we_vote_id: linkedCampaignXWeVoteId, + political_party: politicalParty, + politician_name: ballotItemDisplayName, + we_vote_hosted_profile_image_url_large: politicianImageUrlLarge, + } = politician; + this.setState({ + ballotItemDisplayName, + linkedCampaignXWeVoteId, + politicalParty, + politicianImageUrlLarge, + }); + } + } + // handleResizeLocal () { // if (handleResize('Footer')) { // // console.log('Footer handleResizeEntry update'); @@ -343,7 +386,7 @@ class OrganizationModal extends Component { const { ballotItemWeVoteId, classes, hideBallotItemInfo, hidePositions, params } = this.props; const { allCachedPositionsForThisBallotItem, ballotItemDisplayName, - isCandidate, isMeasure, linkedCampaignXWeVoteId, modalOpen, + isCandidate, isMeasure, isPolitician, linkedCampaignXWeVoteId, modalOpen, politicianWeVoteId, scrolledDown, unFurlPositions, politicalParty, politicianImageUrlLarge, } = this.state; const avatarBackgroundImage = normalizedImagePath('../img/global/svg-icons/avatar-generic.svg'); @@ -433,7 +476,7 @@ class OrganizationModal extends Component { - {(isCandidate && !hideBallotItemInfo) && ( + {((isCandidate || isPolitician) && !hideBallotItemInfo) && ( }> - - - - - - - )}> {/* CORDOVA_TOKEN_AT_CLOSE_OF_A_MULTI_LINE_FALLBACK_DO_NOT_REMOVE */} - - + {isCandidate && ( + + + + + + + )}> {/* CORDOVA_TOKEN_AT_CLOSE_OF_A_MULTI_LINE_FALLBACK_DO_NOT_REMOVE */} + + + )} )} @@ -470,7 +515,7 @@ class OrganizationModal extends Component { )} - { (!hidePositions || unFurlPositions) && ( + { !isPolitician && (!hidePositions || unFurlPositions) && ( <> @@ -487,13 +532,15 @@ class OrganizationModal extends Component { )} - - - - { !!(allCachedPositionsForThisBallotItem.length) && ( + {!isPolitician && ( + + + + )} + { !isPolitician && !!(allCachedPositionsForThisBallotItem.length) && ( <> { !hidePositions || unFurlPositions ? (