perf: page large team sections

This commit is contained in:
777genius 2026-05-29 15:30:27 +03:00
parent 2793dfc5b2
commit 0c6bf5dd33

View file

@ -93,6 +93,9 @@ const LaunchTeamDialog = lazy(() =>
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog })) import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
); );
const TEAM_SECTION_INITIAL_VISIBLE_COUNT = 24;
const TEAM_SECTION_PAGE_SIZE = 24;
interface CreateTeamDialogLoadingFallbackProps { interface CreateTeamDialogLoadingFallbackProps {
readonly isCopy: boolean; readonly isCopy: boolean;
readonly onClose: () => void; readonly onClose: () => void;
@ -515,12 +518,16 @@ const ActiveTeamCard = ({
export const TeamListView = memo(function TeamListView(): React.JSX.Element { export const TeamListView = memo(function TeamListView(): React.JSX.Element {
const { isLight } = useTheme(); const { isLight } = useTheme();
const { t } = useAppTranslation('team'); const { t } = useAppTranslation('team');
const { t: tCommon } = useAppTranslation('common');
const electronMode = isElectronMode(); const electronMode = isElectronMode();
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const [copyData, setCopyData] = useState<TeamCopyData | null>(null); const [copyData, setCopyData] = useState<TeamCopyData | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [filter, setFilter] = useState<TeamListFilterState>(EMPTY_TEAM_FILTER); const [filter, setFilter] = useState<TeamListFilterState>(EMPTY_TEAM_FILTER);
const [aliveTeams, setAliveTeams] = useState<string[]>([]); const [aliveTeams, setAliveTeams] = useState<string[]>([]);
const [teamSectionVisibleCountByKey, setTeamSectionVisibleCountByKey] = useState<
Record<string, number>
>({});
const { const {
teams, teams,
teamsLoading, teamsLoading,
@ -1207,10 +1214,17 @@ export const TeamListView = memo(function TeamListView(): React.JSX.Element {
const activeFiltered = filteredTeams.filter((t) => !t.deletedAt); const activeFiltered = filteredTeams.filter((t) => !t.deletedAt);
const deletedFiltered = filteredTeams.filter((t) => t.deletedAt); const deletedFiltered = filteredTeams.filter((t) => t.deletedAt);
const shouldPageTeamSections = !searchQuery.trim() && !hasActiveFilters;
const selectedProjectSectionKey = currentProjectPath
? `project:${normalizePath(currentProjectPath)}`
: 'project';
const otherTeamsSectionKey = currentProjectPath
? `other:${normalizePath(currentProjectPath)}`
: 'other';
const activeSections = currentProjectPath const activeSections = currentProjectPath
? [ ? [
{ {
key: 'project', key: selectedProjectSectionKey,
title: t('list.sections.projectTeams', { title: t('list.sections.projectTeams', {
project: folderName(currentProjectPath) || t('list.sections.selectedProject'), project: folderName(currentProjectPath) || t('list.sections.selectedProject'),
}), }),
@ -1219,7 +1233,7 @@ export const TeamListView = memo(function TeamListView(): React.JSX.Element {
), ),
}, },
{ {
key: 'other', key: otherTeamsSectionKey,
title: t('list.sections.otherTeams'), title: t('list.sections.otherTeams'),
teams: activeFiltered.filter( teams: activeFiltered.filter(
(team) => !teamMatchesProjectSelection(team, currentProjectPath) (team) => !teamMatchesProjectSelection(team, currentProjectPath)
@ -1238,58 +1252,113 @@ export const TeamListView = memo(function TeamListView(): React.JSX.Element {
<> <>
{activeSections.map((section, sectionIndex) => ( {activeSections.map((section, sectionIndex) => (
<section key={section.key} className={sectionIndex > 0 ? 'mt-6' : undefined}> <section key={section.key} className={sectionIndex > 0 ? 'mt-6' : undefined}>
{section.title ? ( {(() => {
<div className="mb-2 flex items-center gap-2"> const paged =
<h3 className="text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]"> shouldPageTeamSections && section.teams.length > TEAM_SECTION_INITIAL_VISIBLE_COUNT;
{section.title} const requestedVisibleCount =
</h3> teamSectionVisibleCountByKey[section.key] ?? TEAM_SECTION_INITIAL_VISIBLE_COUNT;
<span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-overlay)] px-1.5 py-0.5 text-[10px] font-medium leading-none text-[var(--color-text-secondary)]"> const visibleCount = paged
{section.teams.length} ? Math.min(section.teams.length, requestedVisibleCount)
</span> : section.teams.length;
</div> const visibleTeams = section.teams.slice(0, visibleCount);
) : null} const canShowMore = paged && visibleCount < section.teams.length;
<div className="team-row-zebra-grid grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3"> const canShowLess = paged && visibleCount > TEAM_SECTION_INITIAL_VISIBLE_COUNT;
{section.teams.map((team) => {
const status = resolveTeamStatus( return (
team, <>
team.teamName, {section.title ? (
aliveTeams, <div className="mb-2 flex items-center gap-2">
getCurrentProvisioningProgressForTeam(provisioningState, team.teamName), <h3 className="text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
leadActivityByTeam {section.title}
); </h3>
const teamColorSet = team.color <span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-overlay)] px-1.5 py-0.5 text-[10px] font-medium leading-none text-[var(--color-text-secondary)]">
? getTeamColorSet(team.color) {section.teams.length}
: nameColorSet(team.displayName); </span>
const matchesCurrentProject = currentProjectPath </div>
? teamMatchesProjectSelection(team, currentProjectPath) ) : null}
: false; <div className="team-row-zebra-grid grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
return ( {visibleTeams.map((team) => {
<ActiveTeamCard const status = resolveTeamStatus(
key={team.teamName} team,
team={team} team.teamName,
status={status} aliveTeams,
teamColorSet={teamColorSet} getCurrentProvisioningProgressForTeam(provisioningState, team.teamName),
isLight={isLight} leadActivityByTeam
matchesCurrentProject={matchesCurrentProject} );
currentProjectPath={currentProjectPath} const teamColorSet = team.color
branchName={ ? getTeamColorSet(team.color)
team.projectPath : nameColorSet(team.displayName);
? (branchByPath[normalizePath(team.projectPath)] ?? undefined) const matchesCurrentProject = currentProjectPath
: undefined ? teamMatchesProjectSelection(team, currentProjectPath)
} : false;
taskCounts={taskCountsByTeam.get(team.teamName)} return (
launchingTeamName={launchingTeamName} <ActiveTeamCard
stoppingTeamName={stoppingTeamName} key={team.teamName}
onOpenTeam={openTeamTab} team={team}
onLaunchTeam={handleLaunchTeam} status={status}
onStopTeam={handleStopTeam} teamColorSet={teamColorSet}
onCopyTeam={handleCopyTeam} isLight={isLight}
onDeleteTeam={handleDeleteTeam} matchesCurrentProject={matchesCurrentProject}
t={t} currentProjectPath={currentProjectPath}
/> branchName={
); team.projectPath
})} ? (branchByPath[normalizePath(team.projectPath)] ?? undefined)
</div> : undefined
}
taskCounts={taskCountsByTeam.get(team.teamName)}
launchingTeamName={launchingTeamName}
stoppingTeamName={stoppingTeamName}
onOpenTeam={openTeamTab}
onLaunchTeam={handleLaunchTeam}
onStopTeam={handleStopTeam}
onCopyTeam={handleCopyTeam}
onDeleteTeam={handleDeleteTeam}
t={t}
/>
);
})}
</div>
{(canShowMore || canShowLess) && (
<div className="mt-3 flex items-center justify-center gap-3">
{canShowMore ? (
<button
type="button"
className="rounded px-2.5 py-1 text-xs font-medium text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={() =>
setTeamSectionVisibleCountByKey((prev) => ({
...prev,
[section.key]: Math.min(
section.teams.length,
visibleCount + TEAM_SECTION_PAGE_SIZE
),
}))
}
>
{tCommon('actions.showMore')}
</button>
) : null}
{canShowLess ? (
<button
type="button"
className="rounded px-2.5 py-1 text-xs text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={() =>
setTeamSectionVisibleCountByKey((prev) => ({
...prev,
[section.key]: Math.max(
TEAM_SECTION_INITIAL_VISIBLE_COUNT,
visibleCount - TEAM_SECTION_PAGE_SIZE
),
}))
}
>
{tCommon('actions.showLess')}
</button>
) : null}
</div>
)}
</>
);
})()}
</section> </section>
))} ))}