fix: sync team project filters with app selection
This commit is contained in:
parent
487dcabff5
commit
43f2426a9b
5 changed files with 400 additions and 54 deletions
|
|
@ -11,12 +11,10 @@ import { Filter } from 'lucide-react';
|
|||
import type { TeamSummary } from '@shared/types';
|
||||
|
||||
export interface TeamListFilterState {
|
||||
selectedProjects: Set<string>;
|
||||
selectedStatuses: Set<string>;
|
||||
}
|
||||
|
||||
export const EMPTY_TEAM_FILTER: TeamListFilterState = {
|
||||
selectedProjects: new Set(),
|
||||
selectedStatuses: new Set(),
|
||||
};
|
||||
|
||||
|
|
@ -26,31 +24,38 @@ function folderName(fullPath: string): string {
|
|||
|
||||
interface TeamListFilterPopoverProps {
|
||||
filter: TeamListFilterState;
|
||||
selectedProjectPath: string | null;
|
||||
teams: TeamSummary[];
|
||||
aliveTeams: string[];
|
||||
onFilterChange: (filter: TeamListFilterState) => void;
|
||||
onProjectChange: (projectPath: string | null) => void;
|
||||
}
|
||||
|
||||
export const TeamListFilterPopover = ({
|
||||
filter,
|
||||
selectedProjectPath,
|
||||
teams,
|
||||
aliveTeams,
|
||||
onFilterChange,
|
||||
onProjectChange,
|
||||
}: TeamListFilterPopoverProps): React.JSX.Element => {
|
||||
const activeCount = useMemo(() => {
|
||||
let count = 0;
|
||||
if (filter.selectedStatuses.size > 0) count += 1;
|
||||
if (filter.selectedProjects.size > 0) count += 1;
|
||||
if (selectedProjectPath) count += 1;
|
||||
return count;
|
||||
}, [filter.selectedStatuses, filter.selectedProjects]);
|
||||
}, [filter.selectedStatuses, selectedProjectPath]);
|
||||
|
||||
const uniqueProjects = useMemo(() => {
|
||||
const paths = new Set<string>();
|
||||
for (const team of teams) {
|
||||
if (team.projectPath?.trim()) paths.add(team.projectPath.trim());
|
||||
}
|
||||
if (selectedProjectPath?.trim()) {
|
||||
paths.add(selectedProjectPath.trim());
|
||||
}
|
||||
return [...paths].sort((a, b) => folderName(a).localeCompare(folderName(b)));
|
||||
}, [teams]);
|
||||
}, [selectedProjectPath, teams]);
|
||||
|
||||
const handleStatusToggle = (status: string): void => {
|
||||
const next = new Set(filter.selectedStatuses);
|
||||
|
|
@ -63,17 +68,12 @@ export const TeamListFilterPopover = ({
|
|||
};
|
||||
|
||||
const handleProjectToggle = (project: string): void => {
|
||||
const next = new Set(filter.selectedProjects);
|
||||
if (next.has(project)) {
|
||||
next.delete(project);
|
||||
} else {
|
||||
next.add(project);
|
||||
}
|
||||
onFilterChange({ ...filter, selectedProjects: next });
|
||||
onProjectChange(selectedProjectPath === project ? null : project);
|
||||
};
|
||||
|
||||
const handleClearAll = (): void => {
|
||||
onFilterChange(EMPTY_TEAM_FILTER);
|
||||
onProjectChange(null);
|
||||
};
|
||||
|
||||
const aliveSet = useMemo(() => new Set(aliveTeams), [aliveTeams]);
|
||||
|
|
@ -156,7 +156,7 @@ export const TeamListFilterPopover = ({
|
|||
title={project}
|
||||
>
|
||||
<Checkbox
|
||||
checked={filter.selectedProjects.has(project)}
|
||||
checked={selectedProjectPath === project}
|
||||
onCheckedChange={() => handleProjectToggle(project)}
|
||||
/>
|
||||
<span className="truncate">{folderName(project)}</span>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import {
|
|||
getCurrentProvisioningProgressForTeam,
|
||||
isTeamProvisioningActive,
|
||||
} from '@renderer/store/slices/teamSlice';
|
||||
import {
|
||||
getProjectSelectionResetState,
|
||||
getWorktreeNavigationState,
|
||||
} from '@renderer/store/utils/stateResetHelpers';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { buildTaskCountsByTeam, normalizePath } from '@renderer/utils/pathNormalize';
|
||||
import { getBaseName } from '@renderer/utils/pathUtils';
|
||||
|
|
@ -42,6 +46,11 @@ import { CreateTeamDialog } from './dialogs/CreateTeamDialog';
|
|||
import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog';
|
||||
import { TeamEmptyState } from './TeamEmptyState';
|
||||
import { EMPTY_TEAM_FILTER, TeamListFilterPopover } from './TeamListFilterPopover';
|
||||
import {
|
||||
findTeamProjectSelectionTarget,
|
||||
resolveTeamProjectSelection,
|
||||
teamMatchesProjectSelection,
|
||||
} from './teamProjectSelection';
|
||||
|
||||
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
|
||||
import type { TeamListFilterState } from './TeamListFilterPopover';
|
||||
|
|
@ -256,10 +265,10 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
projects,
|
||||
globalTasks,
|
||||
fetchAllTasks,
|
||||
viewMode,
|
||||
repositoryGroups,
|
||||
selectedRepositoryId,
|
||||
selectedWorktreeId,
|
||||
selectedProjectId,
|
||||
activeProjectId,
|
||||
branchByPath,
|
||||
} = useStore(
|
||||
|
|
@ -275,10 +284,10 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
projects: s.projects,
|
||||
globalTasks: s.globalTasks,
|
||||
fetchAllTasks: s.fetchAllTasks,
|
||||
viewMode: s.viewMode,
|
||||
repositoryGroups: s.repositoryGroups,
|
||||
selectedRepositoryId: s.selectedRepositoryId,
|
||||
selectedWorktreeId: s.selectedWorktreeId,
|
||||
selectedProjectId: s.selectedProjectId,
|
||||
activeProjectId: s.activeProjectId,
|
||||
branchByPath: s.branchByPath,
|
||||
}))
|
||||
|
|
@ -361,23 +370,26 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
};
|
||||
}, [electronMode, showCreateDialog]);
|
||||
|
||||
const currentProjectPath = useMemo(() => {
|
||||
if (viewMode === 'grouped') {
|
||||
const repo = repositoryGroups.find((r) => r.id === selectedRepositoryId);
|
||||
const worktree = repo?.worktrees.find((w) => w.id === selectedWorktreeId);
|
||||
const path = worktree?.path ?? null;
|
||||
return path ? normalizePath(path) : null;
|
||||
}
|
||||
const project = projects.find((p) => p.id === activeProjectId);
|
||||
return project ? normalizePath(project.path) : null;
|
||||
}, [
|
||||
viewMode,
|
||||
repositoryGroups,
|
||||
selectedRepositoryId,
|
||||
selectedWorktreeId,
|
||||
projects,
|
||||
activeProjectId,
|
||||
]);
|
||||
const currentProjectSelection = useMemo(
|
||||
() =>
|
||||
resolveTeamProjectSelection({
|
||||
repositoryGroups,
|
||||
projects,
|
||||
selectedRepositoryId,
|
||||
selectedWorktreeId,
|
||||
selectedProjectId,
|
||||
activeProjectId,
|
||||
}),
|
||||
[
|
||||
repositoryGroups,
|
||||
projects,
|
||||
selectedRepositoryId,
|
||||
selectedWorktreeId,
|
||||
selectedProjectId,
|
||||
activeProjectId,
|
||||
]
|
||||
);
|
||||
const currentProjectPath = currentProjectSelection.projectPath;
|
||||
|
||||
const filteredTeams = useMemo<TeamSummary[]>(() => {
|
||||
let result = teamsWithProvisioning;
|
||||
|
|
@ -409,21 +421,11 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
});
|
||||
}
|
||||
|
||||
if (filter.selectedProjects.size > 0) {
|
||||
result = result.filter(
|
||||
(t) => t.projectPath != null && filter.selectedProjects.has(t.projectPath.trim())
|
||||
);
|
||||
if (currentProjectPath) {
|
||||
result = result.filter((team) => teamMatchesProjectSelection(team, currentProjectPath));
|
||||
}
|
||||
|
||||
const aliveSet = new Set(aliveTeams);
|
||||
const matchesProject = currentProjectPath
|
||||
? (t: TeamSummary): boolean => {
|
||||
if (t.projectPath && normalizePath(t.projectPath) === currentProjectPath) return true;
|
||||
return (
|
||||
t.projectPathHistory?.some((p) => normalizePath(p) === currentProjectPath) ?? false
|
||||
);
|
||||
}
|
||||
: null;
|
||||
|
||||
result = [...result].sort((a, b) => {
|
||||
// 1. Alive (running) teams first
|
||||
|
|
@ -431,19 +433,12 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
const aliveB = aliveSet.has(b.teamName) ? 0 : 1;
|
||||
if (aliveA !== aliveB) return aliveA - aliveB;
|
||||
|
||||
// 2. Matching current project second
|
||||
if (matchesProject) {
|
||||
const projA = matchesProject(a) ? 0 : 1;
|
||||
const projB = matchesProject(b) ? 0 : 1;
|
||||
if (projA !== projB) return projA - projB;
|
||||
}
|
||||
|
||||
// 3. Most recently active teams first (stable secondary sort)
|
||||
// 2. Most recently active teams first (stable secondary sort)
|
||||
const tsA = a.lastActivity ? new Date(a.lastActivity).getTime() : 0;
|
||||
const tsB = b.lastActivity ? new Date(b.lastActivity).getTime() : 0;
|
||||
if (tsA !== tsB) return tsB - tsA;
|
||||
|
||||
// 4. Fallback: alphabetical by team name for deterministic order
|
||||
// 3. Fallback: alphabetical by team name for deterministic order
|
||||
return a.teamName.localeCompare(b.teamName);
|
||||
});
|
||||
|
||||
|
|
@ -459,6 +454,34 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
leadActivityByTeam,
|
||||
]);
|
||||
|
||||
const handleProjectSelectionChange = useCallback(
|
||||
(projectPath: string | null): void => {
|
||||
if (!projectPath) {
|
||||
useStore.setState(getProjectSelectionResetState());
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findTeamProjectSelectionTarget(repositoryGroups, projects, projectPath);
|
||||
if (!target) {
|
||||
console.warn('Unable to resolve selected team project path:', projectPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.kind === 'grouped') {
|
||||
useStore.setState(getWorktreeNavigationState(target.repositoryId, target.worktreeId));
|
||||
void useStore.getState().fetchSessionsInitial(target.worktreeId);
|
||||
return;
|
||||
}
|
||||
|
||||
useStore.getState().selectProject(target.projectId);
|
||||
},
|
||||
[projects, repositoryGroups]
|
||||
);
|
||||
|
||||
const handleResetProjectSelection = useCallback((): void => {
|
||||
handleProjectSelectionChange(null);
|
||||
}, [handleProjectSelectionChange]);
|
||||
|
||||
// Fetch branches once for all visible team project paths (no live polling)
|
||||
const teamPaths = useMemo(
|
||||
() => filteredTeams.map((t) => t.projectPath?.trim()).filter(Boolean) as string[],
|
||||
|
|
@ -731,6 +754,35 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
Only available in local Electron mode.
|
||||
</p>
|
||||
) : null}
|
||||
{currentProjectPath ? (
|
||||
<div className="mt-3 flex items-start justify-between gap-3 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||
Selected project
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-sm text-[var(--color-text)]">
|
||||
<FolderOpen size={14} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="truncate font-medium" title={currentProjectPath}>
|
||||
{folderName(currentProjectPath)}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="mt-1 truncate text-xs text-[var(--color-text-muted)]"
|
||||
title={currentProjectPath}
|
||||
>
|
||||
{currentProjectPath}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleResetProjectSelection}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{teamsWithProvisioning.length > 0 ? (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
|
|
@ -749,9 +801,11 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
</div>
|
||||
<TeamListFilterPopover
|
||||
filter={filter}
|
||||
selectedProjectPath={currentProjectPath}
|
||||
teams={teamsWithProvisioning}
|
||||
aliveTeams={aliveTeams}
|
||||
onFilterChange={setFilter}
|
||||
onProjectChange={handleProjectSelectionChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -794,7 +848,7 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
);
|
||||
}
|
||||
|
||||
const hasActiveFilters = filter.selectedStatuses.size > 0 || filter.selectedProjects.size > 0;
|
||||
const hasActiveFilters = filter.selectedStatuses.size > 0 || currentProjectPath != null;
|
||||
if (filteredTeams.length === 0 && (searchQuery.trim() || hasActiveFilters)) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-[var(--color-text-muted)]">
|
||||
|
|
|
|||
156
src/renderer/components/team/teamProjectSelection.ts
Normal file
156
src/renderer/components/team/teamProjectSelection.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||
|
||||
import type { Project, RepositoryGroup } from '@renderer/types/data';
|
||||
import type { TeamSummary } from '@shared/types';
|
||||
|
||||
export interface ResolveTeamProjectSelectionInput {
|
||||
repositoryGroups: readonly RepositoryGroup[];
|
||||
projects: readonly Project[];
|
||||
selectedRepositoryId: string | null;
|
||||
selectedWorktreeId: string | null;
|
||||
selectedProjectId: string | null;
|
||||
activeProjectId: string | null;
|
||||
}
|
||||
|
||||
export interface ResolvedTeamProjectSelection {
|
||||
projectPath: string | null;
|
||||
repositoryId: string | null;
|
||||
worktreeId: string | null;
|
||||
projectId: string | null;
|
||||
}
|
||||
|
||||
export type TeamProjectSelectionTarget =
|
||||
| {
|
||||
kind: 'grouped';
|
||||
repositoryId: string;
|
||||
worktreeId: string;
|
||||
projectPath: string;
|
||||
}
|
||||
| {
|
||||
kind: 'flat';
|
||||
projectId: string;
|
||||
projectPath: string;
|
||||
};
|
||||
|
||||
function findWorktreeSelection(
|
||||
repositoryGroups: readonly RepositoryGroup[],
|
||||
worktreeId: string
|
||||
): { repositoryId: string; worktreeId: string; projectPath: string } | null {
|
||||
for (const repositoryGroup of repositoryGroups) {
|
||||
const worktree = repositoryGroup.worktrees.find((candidate) => candidate.id === worktreeId);
|
||||
if (worktree) {
|
||||
return {
|
||||
repositoryId: repositoryGroup.id,
|
||||
worktreeId: worktree.id,
|
||||
projectPath: worktree.path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveTeamProjectSelection({
|
||||
repositoryGroups,
|
||||
projects,
|
||||
selectedRepositoryId,
|
||||
selectedWorktreeId,
|
||||
selectedProjectId,
|
||||
activeProjectId,
|
||||
}: ResolveTeamProjectSelectionInput): ResolvedTeamProjectSelection {
|
||||
const effectiveWorktreeId = selectedWorktreeId ?? activeProjectId ?? selectedProjectId ?? null;
|
||||
if (effectiveWorktreeId) {
|
||||
const worktreeSelection = findWorktreeSelection(repositoryGroups, effectiveWorktreeId);
|
||||
if (worktreeSelection) {
|
||||
return {
|
||||
projectPath: worktreeSelection.projectPath,
|
||||
repositoryId: worktreeSelection.repositoryId,
|
||||
worktreeId: worktreeSelection.worktreeId,
|
||||
projectId: worktreeSelection.worktreeId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveProjectId = activeProjectId ?? selectedProjectId ?? null;
|
||||
if (effectiveProjectId) {
|
||||
const project = projects.find((candidate) => candidate.id === effectiveProjectId);
|
||||
if (project) {
|
||||
return {
|
||||
projectPath: project.path,
|
||||
repositoryId: null,
|
||||
worktreeId: null,
|
||||
projectId: project.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedRepositoryId) {
|
||||
const repositoryGroup = repositoryGroups.find(
|
||||
(candidate) => candidate.id === selectedRepositoryId
|
||||
);
|
||||
const fallbackWorktree = repositoryGroup?.worktrees[0] ?? null;
|
||||
if (fallbackWorktree) {
|
||||
return {
|
||||
projectPath: fallbackWorktree.path,
|
||||
repositoryId: repositoryGroup?.id ?? null,
|
||||
worktreeId: fallbackWorktree.id,
|
||||
projectId: fallbackWorktree.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectPath: null,
|
||||
repositoryId: null,
|
||||
worktreeId: null,
|
||||
projectId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function findTeamProjectSelectionTarget(
|
||||
repositoryGroups: readonly RepositoryGroup[],
|
||||
projects: readonly Project[],
|
||||
projectPath: string
|
||||
): TeamProjectSelectionTarget | null {
|
||||
const normalizedProjectPath = normalizePath(projectPath);
|
||||
|
||||
for (const repositoryGroup of repositoryGroups) {
|
||||
const worktree = repositoryGroup.worktrees.find(
|
||||
(candidate) => normalizePath(candidate.path) === normalizedProjectPath
|
||||
);
|
||||
if (worktree) {
|
||||
return {
|
||||
kind: 'grouped',
|
||||
repositoryId: repositoryGroup.id,
|
||||
worktreeId: worktree.id,
|
||||
projectPath: worktree.path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const project = projects.find(
|
||||
(candidate) => normalizePath(candidate.path) === normalizedProjectPath
|
||||
);
|
||||
if (project) {
|
||||
return {
|
||||
kind: 'flat',
|
||||
projectId: project.id,
|
||||
projectPath: project.path,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function teamMatchesProjectSelection(team: TeamSummary, projectPath: string): boolean {
|
||||
const normalizedProjectPath = normalizePath(projectPath);
|
||||
if (team.projectPath && normalizePath(team.projectPath) === normalizedProjectPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
team.projectPathHistory?.some(
|
||||
(candidate) => normalizePath(candidate) === normalizedProjectPath
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
|
@ -39,6 +39,20 @@ export function getWorktreeNavigationState(repoId: string, worktreeId: string):
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the active project/worktree selection without resetting unrelated UI state.
|
||||
* Used when a screen wants to remove the current project context entirely.
|
||||
*/
|
||||
export function getProjectSelectionResetState(): Partial<AppState> {
|
||||
return {
|
||||
selectedRepositoryId: null,
|
||||
selectedWorktreeId: null,
|
||||
selectedProjectId: null,
|
||||
activeProjectId: null,
|
||||
...getSessionResetState(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full state reset (session + project + repository + conversation).
|
||||
* Used when closing all tabs or resetting to initial state.
|
||||
|
|
|
|||
122
test/renderer/components/team/teamProjectSelection.test.ts
Normal file
122
test/renderer/components/team/teamProjectSelection.test.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
findTeamProjectSelectionTarget,
|
||||
resolveTeamProjectSelection,
|
||||
teamMatchesProjectSelection,
|
||||
} from '@renderer/components/team/teamProjectSelection';
|
||||
|
||||
import type { Project, RepositoryGroup } from '@renderer/types/data';
|
||||
import type { TeamSummary } from '@shared/types';
|
||||
|
||||
const repositoryGroups: RepositoryGroup[] = [
|
||||
{
|
||||
id: 'repo-headless',
|
||||
identity: null,
|
||||
name: 'headless',
|
||||
mostRecentSession: 1,
|
||||
totalSessions: 5,
|
||||
worktrees: [
|
||||
{
|
||||
id: 'wt-headless',
|
||||
path: '/Users/test/headless',
|
||||
name: 'headless',
|
||||
isMainWorktree: true,
|
||||
source: 'git',
|
||||
sessions: [],
|
||||
totalSessions: 5,
|
||||
createdAt: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const projects: Project[] = [
|
||||
{
|
||||
id: 'project-standalone',
|
||||
name: 'standalone',
|
||||
path: '/Users/test/standalone',
|
||||
sessions: [],
|
||||
totalSessions: 2,
|
||||
createdAt: 1,
|
||||
},
|
||||
];
|
||||
|
||||
describe('teamProjectSelection', () => {
|
||||
it('resolves selected grouped worktree path', () => {
|
||||
expect(
|
||||
resolveTeamProjectSelection({
|
||||
repositoryGroups,
|
||||
projects,
|
||||
selectedRepositoryId: 'repo-headless',
|
||||
selectedWorktreeId: 'wt-headless',
|
||||
selectedProjectId: 'wt-headless',
|
||||
activeProjectId: 'wt-headless',
|
||||
})
|
||||
).toEqual({
|
||||
projectPath: '/Users/test/headless',
|
||||
repositoryId: 'repo-headless',
|
||||
worktreeId: 'wt-headless',
|
||||
projectId: 'wt-headless',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to active project id when grouped ids are stale', () => {
|
||||
expect(
|
||||
resolveTeamProjectSelection({
|
||||
repositoryGroups,
|
||||
projects,
|
||||
selectedRepositoryId: null,
|
||||
selectedWorktreeId: null,
|
||||
selectedProjectId: null,
|
||||
activeProjectId: 'wt-headless',
|
||||
})
|
||||
).toEqual({
|
||||
projectPath: '/Users/test/headless',
|
||||
repositoryId: 'repo-headless',
|
||||
worktreeId: 'wt-headless',
|
||||
projectId: 'wt-headless',
|
||||
});
|
||||
});
|
||||
|
||||
it('finds grouped selection target by project path', () => {
|
||||
expect(
|
||||
findTeamProjectSelectionTarget(repositoryGroups, projects, '/users/test/headless/')
|
||||
).toEqual({
|
||||
kind: 'grouped',
|
||||
repositoryId: 'repo-headless',
|
||||
worktreeId: 'wt-headless',
|
||||
projectPath: '/Users/test/headless',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to flat projects when no grouped worktree exists', () => {
|
||||
expect(
|
||||
findTeamProjectSelectionTarget(repositoryGroups, projects, '/users/test/standalone/')
|
||||
).toEqual({
|
||||
kind: 'flat',
|
||||
projectId: 'project-standalone',
|
||||
projectPath: '/Users/test/standalone',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches team project history against the selected project', () => {
|
||||
const team = {
|
||||
teamName: 'demo-team',
|
||||
displayName: 'Demo Team',
|
||||
description: '',
|
||||
color: null,
|
||||
deletedAt: null,
|
||||
pendingCreate: false,
|
||||
partialLaunchFailure: false,
|
||||
teamLaunchState: null,
|
||||
projectPath: '/Users/test/other',
|
||||
projectPathHistory: ['/Users/test/headless', '/Users/test/archive'],
|
||||
lastActivity: null,
|
||||
members: [],
|
||||
} as TeamSummary;
|
||||
|
||||
expect(teamMatchesProjectSelection(team, '/users/test/headless')).toBe(true);
|
||||
expect(teamMatchesProjectSelection(team, '/users/test/missing')).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue