fix(team): dedupe project path options
This commit is contained in:
parent
358496c353
commit
83748673af
3 changed files with 238 additions and 120 deletions
|
|
@ -8,6 +8,8 @@ import { Label } from '@renderer/components/ui/label';
|
|||
import { cn } from '@renderer/lib/utils';
|
||||
import { Check, FolderOpen } from 'lucide-react';
|
||||
|
||||
import { buildProjectPathOptions } from './projectPathOptions';
|
||||
|
||||
import type { Project } from '@shared/types';
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
|
|
@ -69,7 +71,13 @@ export const ProjectPathSelector = ({
|
|||
projectsLoading,
|
||||
projectsError,
|
||||
fieldError,
|
||||
}: ProjectPathSelectorProps): React.JSX.Element => (
|
||||
}: ProjectPathSelectorProps): React.JSX.Element => {
|
||||
const projectOptions = React.useMemo(
|
||||
() => buildProjectPathOptions(projects, selectedProjectPath),
|
||||
[projects, selectedProjectPath]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label>Project</Label>
|
||||
<div className="space-y-2">
|
||||
|
|
@ -108,17 +116,13 @@ export const ProjectPathSelector = ({
|
|||
<FolderOpen size={16} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Combobox
|
||||
options={projects.map((project) => ({
|
||||
value: project.path,
|
||||
label: project.name,
|
||||
description: project.path,
|
||||
}))}
|
||||
options={projectOptions}
|
||||
value={selectedProjectPath}
|
||||
onValueChange={onSelectedProjectPathChange}
|
||||
placeholder={projectsLoading ? 'Loading projects...' : 'Select a project...'}
|
||||
searchPlaceholder="Search project by name or path"
|
||||
emptyMessage="Nothing found"
|
||||
disabled={projectsLoading || projects.length === 0}
|
||||
disabled={projectsLoading || projectOptions.length === 0}
|
||||
renderOption={(option, isSelected, query) => (
|
||||
<>
|
||||
<Check
|
||||
|
|
@ -146,7 +150,7 @@ export const ProjectPathSelector = ({
|
|||
</p>
|
||||
) : null}
|
||||
{projectsError ? <p className="text-[11px] text-red-300">{projectsError}</p> : null}
|
||||
{!projectsLoading && projects.length === 0 ? (
|
||||
{!projectsLoading && projectOptions.length === 0 ? (
|
||||
<p className="text-[11px]" style={{ color: 'var(--warning-text)' }}>
|
||||
No projects found, switch to custom path.
|
||||
</p>
|
||||
|
|
@ -174,7 +178,7 @@ export const ProjectPathSelector = ({
|
|||
onCustomCwdChange(paths[0]);
|
||||
}
|
||||
} catch {
|
||||
// IPC error — dialog may have been cancelled or failed
|
||||
// IPC error - dialog may have been cancelled or failed
|
||||
}
|
||||
})();
|
||||
}}
|
||||
|
|
@ -196,4 +200,5 @@ export const ProjectPathSelector = ({
|
|||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
|
|
|||
45
src/renderer/components/team/dialogs/projectPathOptions.ts
Normal file
45
src/renderer/components/team/dialogs/projectPathOptions.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||
|
||||
import type { ComboboxOption } from '@renderer/components/ui/combobox';
|
||||
import type { Project } from '@shared/types';
|
||||
|
||||
function toProjectOption(project: Project): ComboboxOption {
|
||||
return {
|
||||
value: project.path,
|
||||
label: project.name,
|
||||
description: project.path,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse duplicate project entries that resolve to the same filesystem path.
|
||||
* This keeps combobox item values unique even when scanner sources overlap.
|
||||
*/
|
||||
export function buildProjectPathOptions(
|
||||
projects: Project[],
|
||||
preferredPath?: string
|
||||
): ComboboxOption[] {
|
||||
const options: ComboboxOption[] = [];
|
||||
const optionIndexByNormalizedPath = new Map<string, number>();
|
||||
const normalizedPreferredPath = preferredPath ? normalizePath(preferredPath) : null;
|
||||
|
||||
for (const project of projects) {
|
||||
const normalizedProjectPath = normalizePath(project.path);
|
||||
const existingIndex = optionIndexByNormalizedPath.get(normalizedProjectPath);
|
||||
|
||||
if (existingIndex === undefined) {
|
||||
optionIndexByNormalizedPath.set(normalizedProjectPath, options.length);
|
||||
options.push(toProjectOption(project));
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldPreferCurrentOption =
|
||||
normalizedPreferredPath === normalizedProjectPath && project.path === preferredPath;
|
||||
|
||||
if (shouldPreferCurrentOption) {
|
||||
options[existingIndex] = toProjectOption(project);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildProjectPathOptions } from '@renderer/components/team/dialogs/projectPathOptions';
|
||||
|
||||
import type { Project } from '@shared/types';
|
||||
|
||||
function createProject(overrides: Partial<Project>): Project {
|
||||
return {
|
||||
id: 'project-id',
|
||||
name: 'project',
|
||||
path: '/Users/test/project',
|
||||
sessions: [],
|
||||
totalSessions: 0,
|
||||
createdAt: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildProjectPathOptions', () => {
|
||||
it('removes duplicate projects that point to the same path', () => {
|
||||
const options = buildProjectPathOptions([
|
||||
createProject({
|
||||
id: 'project-1',
|
||||
name: 'lintai',
|
||||
path: '/Users/belief/dev/projects/lintai',
|
||||
}),
|
||||
createProject({
|
||||
id: 'project-2',
|
||||
name: 'lintai duplicate',
|
||||
path: '/Users/belief/dev/projects/lintai',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(options).toEqual([
|
||||
{
|
||||
value: '/Users/belief/dev/projects/lintai',
|
||||
label: 'lintai',
|
||||
description: '/Users/belief/dev/projects/lintai',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers the currently selected variant when duplicate paths normalize equally', () => {
|
||||
const options = buildProjectPathOptions(
|
||||
[
|
||||
createProject({
|
||||
id: 'project-1',
|
||||
name: 'LintAI',
|
||||
path: '/Users/Belief/dev/projects/lintai',
|
||||
}),
|
||||
createProject({
|
||||
id: 'project-2',
|
||||
name: 'lintai',
|
||||
path: '/Users/belief/dev/projects/lintai/',
|
||||
}),
|
||||
],
|
||||
'/Users/belief/dev/projects/lintai/'
|
||||
);
|
||||
|
||||
expect(options).toEqual([
|
||||
{
|
||||
value: '/Users/belief/dev/projects/lintai/',
|
||||
label: 'lintai',
|
||||
description: '/Users/belief/dev/projects/lintai/',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue