perf: page large team sections
This commit is contained in:
parent
2793dfc5b2
commit
0c6bf5dd33
1 changed files with 123 additions and 54 deletions
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue