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 ? (