diff --git a/src/components/Teams/Team.jsx b/src/components/Teams/Team.jsx index dc817fb51a..ac479edbaa 100644 --- a/src/components/Teams/Team.jsx +++ b/src/components/Teams/Team.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/destructuring-assignment */ import styles from './Team.module.css'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { connect, useSelector, useDispatch } from 'react-redux'; import { Button } from 'reactstrap'; @@ -77,40 +77,31 @@ export function Team({ // string key for cache/DOM ids const teamIdKey = String(props.teamId ?? ''); + // localMembers is only populated after the user opens the modal or hovers. + // On mount we do NOT pre-fetch — that was firing GET /api/team/:id/users + // for every single row on page load (thousands of requests = 30s load time). + // Member counts are computed from props.team.members which already comes + // from the getAllTeams aggregation, so no extra request is needed. const [localMembers, setLocalMembers] = useState(() => getCachedTeamMembers(teamIdKey) || null); - const [loading, setLoading] = useState(false); - - useEffect(() => { - const cached = getCachedTeamMembers(teamIdKey); - if (cached && !localMembers) setLocalMembers(cached); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [teamIdKey]); - - useEffect(() => { - if (!getCachedTeamMembers(teamIdKey) && teamIdKey) { - fetchTeamMembersCached(dispatch, getTeamMembers, teamIdKey) - .then(setLocalMembers) - .catch(() => {}); - } - }, [dispatch, teamIdKey]); const members = localMembers ?? props.team?.members ?? []; - const { total, active: activeCount, inactive } = computeCounts(members, loading, localMembers); + const { total, active: activeCount, inactive } = computeCounts(members, false, localMembers); - // Fire callback immediately (keeps tests & UX snappy), then refresh members + // Fire callback immediately (keeps tests & UX snappy), then refresh members. + // We do NOT reset localMembers to null or set loading=true here — doing so + // caused the count to flash back to '…' on every click. Since props.team.members + // is always available as a fallback, the count stays stable while the fresh + // fetch completes in the background. const handleOpenMembers = () => { clearCachedTeamMembers(teamIdKey); - setLocalMembers(null); // reset stale local state if (typeof props.onMembersClick === 'function') { props.onMembersClick(teamIdRaw, props.name, props.teamCode); // don't pass localMembers } - setLoading(true); fetchTeamMembersCached(dispatch, getTeamMembers, teamIdKey) .then(fresh => { setLocalMembers(fresh); }) - .catch(() => {}) - .finally(() => setLoading(false)); + .catch(() => {}); }; return ( @@ -165,9 +156,8 @@ export function Team({ onClick={handleOpenMembers} data-testid="members-btn" aria-label="Users" - disabled={loading} > - {loading ? : } + diff --git a/src/components/Teams/TeamMembersPopup.jsx b/src/components/Teams/TeamMembersPopup.jsx index 9f747a83cb..e1508d0d65 100644 --- a/src/components/Teams/TeamMembersPopup.jsx +++ b/src/components/Teams/TeamMembersPopup.jsx @@ -158,12 +158,29 @@ export const TeamMembersPopup = React.memo(props => { const map = {}; if (Array.isArray(teamsData) && teamsData.length > 0) { for (const m of teamsData[0]?.members || []) { - map[m.userId] = m.visible; + // Backend's getAllTeams aggregation returns each member object + // keyed by `_id` (the user's id), not `userId`. Using the wrong + // key here meant every lookup below resolved to `undefined`, + // which made the "See All" toggle appear to reset itself. + map[m._id] = m.visible; } } return map; }, [props.teamData]); + // Only render toggle rows once teamData AND its members are populated so that + // `choice` is never undefined on first mount — prevents the brief ON flash. + // props.teamData can exist with members: undefined on the first Redux update, + // so we must check members is a non-empty array too. + const isMemberVisibilityReady = useMemo(() => { + return ( + Array.isArray(props.teamData) && + props.teamData.length > 0 && + Array.isArray(props.teamData[0]?.members) && + props.teamData[0].members.length > 0 + ); + }, [props.teamData]); + useEffect(() => { setIsValidUser(true); setDuplicateUserAlert(false); @@ -303,7 +320,7 @@ export const TeamMembersPopup = React.memo(props => { }; const renderBody = () => { - if (showTableSpinner) { + if (showTableSpinner || !isMemberVisibilityReady) { return ( @@ -505,7 +522,7 @@ TeamMembersPopup.propTypes = { PropTypes.shape({ members: PropTypes.arrayOf( PropTypes.shape({ - userId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), visible: PropTypes.bool, }), ), diff --git a/src/components/Teams/Teams.jsx b/src/components/Teams/Teams.jsx index e1028ba9dc..1b26890238 100644 --- a/src/components/Teams/Teams.jsx +++ b/src/components/Teams/Teams.jsx @@ -381,6 +381,7 @@ class Teams extends React.PureComponent { await this.props.updateTeamMemeberVisibility(this.state.selectedTeamId, userId, visibility); const freshMembers = await this.props.getTeamMembers(this.state.selectedTeamId); this.setState({ selectedTeamMembers: freshMembers || [] }); + await this.props.getAllUserTeams(); }; // NOTE: Team component calls (id, name, code) and we open immediately @@ -402,10 +403,14 @@ class Teams extends React.PureComponent { onTeamMembersPopupClose = () => { this.props.clearTeamMembers(); + // Do NOT clear selectedTeamId here — setting it to undefined causes + // getTeamMembers(undefined) on the next toggle, which fires a 400 error + // and briefly wipes selectedTeamData, resetting all toggles to ON. + // selectedTeamId gets overwritten correctly when the next team is opened. this.setState({ - selectedTeamId: undefined, selectedTeam: '', teamMembersPopupOpen: false, + selectedTeamMembers: [], }); }; diff --git a/src/components/Teams/ToggleSwitch/ToggleSwitch.jsx b/src/components/Teams/ToggleSwitch/ToggleSwitch.jsx index 529f44cae6..b9e1fdc9c3 100644 --- a/src/components/Teams/ToggleSwitch/ToggleSwitch.jsx +++ b/src/components/Teams/ToggleSwitch/ToggleSwitch.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import style from './ToggleSwitch.module.scss'; const DEFAULT_VISIBILITY = true; @@ -6,6 +6,11 @@ const DEFAULT_VISIBILITY = true; function ToggleSwitch({ switchType, UpdateTeamMembersVisibility, userId, choice }) { const [visibility, setVisibility] = useState(choice !== undefined ? choice : DEFAULT_VISIBILITY); + useEffect(() => { + console.log('ToggleSwitch choice prop:', userId, choice); + setVisibility(choice !== undefined ? choice : DEFAULT_VISIBILITY); + }, [choice]); + const toggleVisibility = () => { const isChecked = !visibility; setVisibility(isChecked);