refactor(team): extract team data selectors
This commit is contained in:
parent
8589391ccf
commit
1d2f61ad86
3 changed files with 149 additions and 45 deletions
|
|
@ -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
|
||||
|
|
|
|||
47
src/renderer/store/team/teamDataSelectors.ts
Normal file
47
src/renderer/store/team/teamDataSelectors.ts
Normal 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;
|
||||
}
|
||||
95
test/renderer/store/teamDataSelectors.test.ts
Normal file
95
test/renderer/store/teamDataSelectors.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue