refactor(team): extract team data selectors

This commit is contained in:
777genius 2026-05-22 00:06:28 +03:00
parent 8589391ccf
commit 1d2f61ad86
3 changed files with 149 additions and 45 deletions

View file

@ -29,6 +29,7 @@ import {
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { selectTeamDataForName } from '../team/teamDataSelectors';
import {
areInboxMessageArraysEquivalent,
clearTeamMessageSelectorCaches,
@ -95,6 +96,12 @@ import type {
} from '@shared/types';
import type { StateCreator } from 'zustand';
export {
selectTeamDataForName,
selectTeamIsAliveForName,
selectTeamMemberSnapshotsForName,
selectTeamTasksForName,
} from '../team/teamDataSelectors';
export type {
RefreshTeamMessagesHeadResult,
TeamMessagesCacheEntry,
@ -1763,9 +1770,6 @@ const resolvedMemberSelectorCache = new Map<
result: ResolvedTeamMember | null;
}
>();
const EMPTY_TEAM_MEMBER_SNAPSHOTS: TeamMemberSnapshot[] = [];
const EMPTY_TEAM_TASKS: TeamViewSnapshot['tasks'] = [];
function resolveMemberStatus(
snapshot: TeamMemberSnapshot,
activity: MemberActivityMetaEntry | undefined
@ -2187,27 +2191,6 @@ function structurallyShareMemberActivityFacts(
return changed ? shared : previous;
}
type TeamDataSelectorState = Pick<
TeamSlice,
'teamDataCacheByName' | 'selectedTeamName' | 'selectedTeamData'
>;
export function selectTeamDataForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot | null {
if (!teamName) {
return null;
}
if (state.selectedTeamName === teamName && state.selectedTeamData) {
return state.selectedTeamData;
}
return (
state.teamDataCacheByName[teamName] ??
(state.selectedTeamName === teamName ? state.selectedTeamData : null)
);
}
type ResolvedMemberSelectorState = Pick<
TeamSlice,
'teamDataCacheByName' | 'selectedTeamName' | 'selectedTeamData' | 'memberActivityMetaByTeam'
@ -2316,27 +2299,6 @@ export function selectResolvedMemberForTeamName(
return result;
}
export function selectTeamMemberSnapshotsForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot['members'] {
return selectTeamDataForName(state, teamName)?.members ?? EMPTY_TEAM_MEMBER_SNAPSHOTS;
}
export function selectTeamTasksForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot['tasks'] {
return selectTeamDataForName(state, teamName)?.tasks ?? EMPTY_TEAM_TASKS;
}
export function selectTeamIsAliveForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): boolean | undefined {
return selectTeamDataForName(state, teamName)?.isAlive;
}
function isMemberActivityMetaStale(
state: Pick<TeamSlice, 'memberActivityMetaByTeam' | 'teamMessagesByName'>,
teamName: string

View file

@ -0,0 +1,47 @@
import type { TeamViewSnapshot } from '@shared/types';
export interface TeamDataSelectorState {
teamDataCacheByName: Record<string, TeamViewSnapshot>;
selectedTeamName: string | null;
selectedTeamData: TeamViewSnapshot | null;
}
const EMPTY_TEAM_MEMBER_SNAPSHOTS: TeamViewSnapshot['members'] = [];
const EMPTY_TEAM_TASKS: TeamViewSnapshot['tasks'] = [];
export function selectTeamDataForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot | null {
if (!teamName) {
return null;
}
if (state.selectedTeamName === teamName && state.selectedTeamData) {
return state.selectedTeamData;
}
return (
state.teamDataCacheByName[teamName] ??
(state.selectedTeamName === teamName ? state.selectedTeamData : null)
);
}
export function selectTeamMemberSnapshotsForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot['members'] {
return selectTeamDataForName(state, teamName)?.members ?? EMPTY_TEAM_MEMBER_SNAPSHOTS;
}
export function selectTeamTasksForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): TeamViewSnapshot['tasks'] {
return selectTeamDataForName(state, teamName)?.tasks ?? EMPTY_TEAM_TASKS;
}
export function selectTeamIsAliveForName(
state: TeamDataSelectorState,
teamName: string | null | undefined
): boolean | undefined {
return selectTeamDataForName(state, teamName)?.isAlive;
}

View file

@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest';
import {
selectTeamDataForName,
selectTeamIsAliveForName,
selectTeamMemberSnapshotsForName,
selectTeamTasksForName,
type TeamDataSelectorState,
} from '../../../src/renderer/store/team/teamDataSelectors';
import type { TeamViewSnapshot } from '../../../src/shared/types';
function createSnapshot(overrides: Partial<TeamViewSnapshot> = {}): TeamViewSnapshot {
return {
teamName: 'my-team',
config: { name: 'My Team' },
members: [],
tasks: [],
kanbanState: { teamName: 'my-team', reviewers: [], tasks: {} },
processes: [],
...overrides,
};
}
function createState(overrides: Partial<TeamDataSelectorState> = {}): TeamDataSelectorState {
return {
teamDataCacheByName: {},
selectedTeamName: null,
selectedTeamData: null,
...overrides,
};
}
describe('teamDataSelectors', () => {
it('returns null when no team name is selected or cached', () => {
const state = createState();
expect(selectTeamDataForName(state, null)).toBeNull();
expect(selectTeamDataForName(state, undefined)).toBeNull();
expect(selectTeamDataForName(state, 'missing-team')).toBeNull();
});
it('prefers selected team data over cached data for the active team', () => {
const cachedSnapshot = createSnapshot({ teamName: 'my-team', isAlive: false });
const selectedSnapshot = createSnapshot({ teamName: 'my-team', isAlive: true });
const state = createState({
selectedTeamName: 'my-team',
selectedTeamData: selectedSnapshot,
teamDataCacheByName: {
'my-team': cachedSnapshot,
},
});
expect(selectTeamDataForName(state, 'my-team')).toBe(selectedSnapshot);
});
it('falls back to cached team data outside the selected snapshot', () => {
const cachedSnapshot = createSnapshot({ teamName: 'cached-team', isAlive: true });
const state = createState({
selectedTeamName: 'other-team',
selectedTeamData: createSnapshot({ teamName: 'other-team' }),
teamDataCacheByName: {
'cached-team': cachedSnapshot,
},
});
expect(selectTeamDataForName(state, 'cached-team')).toBe(cachedSnapshot);
});
it('returns stable empty arrays and scalar fields from team snapshots', () => {
const task = { id: 'task-1', subject: 'Build', status: 'pending' as const };
const member = { name: 'alice', role: 'developer', currentTaskId: null, taskCount: 0 };
const state = createState({
teamDataCacheByName: {
'my-team': createSnapshot({
members: [member],
tasks: [task],
isAlive: true,
}),
},
});
expect(selectTeamMemberSnapshotsForName(state, 'my-team')).toEqual([member]);
expect(selectTeamTasksForName(state, 'my-team')).toEqual([task]);
expect(selectTeamIsAliveForName(state, 'my-team')).toBe(true);
expect(selectTeamMemberSnapshotsForName(state, 'missing-team')).toBe(
selectTeamMemberSnapshotsForName(state, 'missing-team')
);
expect(selectTeamTasksForName(state, 'missing-team')).toBe(
selectTeamTasksForName(state, 'missing-team')
);
expect(selectTeamIsAliveForName(state, 'missing-team')).toBeUndefined();
});
});