feat(projects): support adding multiple projects at once in the directory picker
Add a multi-select mode to the "Add project directory" dialog: each directory row gets a select toggle (checkbox icon, Space toggles the highlighted row), and the primary action becomes "Add selected" and registers every selected directory in one store update. Selections apply to the currently browsed directory and reset on navigation, dialog open, and clone-mode entry. Clone mode keeps its single-target flow. Add addProjects() to useProjectsStore: validates, normalizes, and dedups paths (already-added or duplicated), creates entries in a single state update and single persist, activates the first newly added project, and discovers icons for each entry. Mirrors addProject semantics for the single entry. Refs OPE-142
This commit is contained in:
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Usage: GitHub Copilot now shows a single AI Credits window, matching Copilot's token-based quota, in place of the old Chat Requests and Completions windows (thanks to @jakoss).
|
- Usage: GitHub Copilot now shows a single AI Credits window, matching Copilot's token-based quota, in place of the old Chat Requests and Completions windows (thanks to @jakoss).
|
||||||
- Settings: fixed the Cloudflare Tunnel download link shown when cloudflared is not installed (thanks to @AyoubAchour).
|
- Settings: fixed the Cloudflare Tunnel download link shown when cloudflared is not installed (thanks to @AyoubAchour).
|
||||||
- Git: picking a remote branch such as `origin/main` in the branch selector now switches you to that branch instead of leaving the repository on a detached `HEAD` with no branch name.
|
- Git: picking a remote branch such as `origin/main` in the branch selector now switches you to that branch instead of leaving the repository on a detached `HEAD` with no branch name.
|
||||||
|
- Projects: the "Add project directory" picker now accepts multiple selections at once — each row has a checkbox (or press Space on the highlighted row) and the primary action adds every selected directory in a single store update, with per-entry validation, deduplication, and one persistence write (thanks to @herjarsa).
|
||||||
- Desktop: "Restart to Update" no longer looks dead when the update cannot be installed — the update window now shows the reason, including when the running copy was not installed from an official signed release, and the button stays available to retry.
|
- Desktop: "Restart to Update" no longer looks dead when the update cannot be installed — the update window now shows the reason, including when the running copy was not installed from an official signed release, and the button stays available to retry.
|
||||||
|
|
||||||
## [1.21.0] - 2026-08-26
|
## [1.21.0] - 2026-08-26
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
const addProject = useProjectsStore((s) => s.addProject);
|
const addProject = useProjectsStore((s) => s.addProject);
|
||||||
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
||||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||||
|
const addProjects = useProjectsStore((s) => s.addProjects);
|
||||||
const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles);
|
const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles);
|
||||||
const globalGitIdentity = useGitIdentitiesStore((s) => s.globalIdentity);
|
const globalGitIdentity = useGitIdentitiesStore((s) => s.globalIdentity);
|
||||||
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
|
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
|
||||||
@@ -177,6 +178,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
const [cloneRemoteUrl, setCloneRemoteUrl] = React.useState('');
|
const [cloneRemoteUrl, setCloneRemoteUrl] = React.useState('');
|
||||||
const [selectedGitIdentityId, setSelectedGitIdentityId] = React.useState<string | null>(null);
|
const [selectedGitIdentityId, setSelectedGitIdentityId] = React.useState<string | null>(null);
|
||||||
const [showHidden, setShowHidden] = React.useState(false);
|
const [showHidden, setShowHidden] = React.useState(false);
|
||||||
|
const [selectedPaths, setSelectedPaths] = React.useState<string[]>([]);
|
||||||
|
|
||||||
const explorerRootDirectory = dialogHomeDirectory || homeDirectory;
|
const explorerRootDirectory = dialogHomeDirectory || homeDirectory;
|
||||||
|
|
||||||
@@ -197,6 +199,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
setCloneRemoteUrl('');
|
setCloneRemoteUrl('');
|
||||||
setSelectedGitIdentityId(null);
|
setSelectedGitIdentityId(null);
|
||||||
setShowHidden(false);
|
setShowHidden(false);
|
||||||
|
setSelectedPaths([]);
|
||||||
requestAnimationFrame(() => focusPathInput(inputRef.current));
|
requestAnimationFrame(() => focusPathInput(inputRef.current));
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -330,6 +333,27 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
setHighlightedIndex(0);
|
setHighlightedIndex(0);
|
||||||
}, [query, rows.length]);
|
}, [query, rows.length]);
|
||||||
|
|
||||||
|
// Selections apply to the currently browsed directory: navigating into
|
||||||
|
// another folder clears the pending batch so the primary action always
|
||||||
|
// reflects the visible picker state.
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelectedPaths([]);
|
||||||
|
}, [browseDirectoryAbsolutePath]);
|
||||||
|
|
||||||
|
const selectionPaths = React.useMemo(
|
||||||
|
() => selectedPaths.filter((path) => {
|
||||||
|
const normalized = normalizeDirectoryPath(path);
|
||||||
|
return Boolean(normalized && !addedProjectPaths.has(normalized));
|
||||||
|
}),
|
||||||
|
[addedProjectPaths, selectedPaths]
|
||||||
|
);
|
||||||
|
|
||||||
|
const togglePathSelection = React.useCallback((path: string) => {
|
||||||
|
setSelectedPaths((prev) => (
|
||||||
|
prev.includes(path) ? prev.filter((entry) => entry !== path) : [...prev, path]
|
||||||
|
));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const targetPath = React.useMemo(() => {
|
const targetPath = React.useMemo(() => {
|
||||||
if (!explorerRootDirectory) return '';
|
if (!explorerRootDirectory) return '';
|
||||||
return trimTrailingSeparators(displayPathToAbsolutePath(query, explorerRootDirectory));
|
return trimTrailingSeparators(displayPathToAbsolutePath(query, explorerRootDirectory));
|
||||||
@@ -351,28 +375,29 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
);
|
);
|
||||||
const canAddProject = !isConfirming
|
const canAddProject = !isConfirming
|
||||||
&& !isOpeningFinder
|
&& !isOpeningFinder
|
||||||
&& !isAlreadyAdded
|
|
||||||
&& browseErrorReason !== 'os-permission'
|
&& browseErrorReason !== 'os-permission'
|
||||||
&& browseErrorReason !== 'invalid-response'
|
&& browseErrorReason !== 'invalid-response'
|
||||||
&& browseErrorReason !== 'unknown'
|
&& browseErrorReason !== 'unknown'
|
||||||
&& Boolean(targetPath);
|
&& ((!isCloneMode && selectionPaths.length > 0) || (!isAlreadyAdded && Boolean(targetPath)));
|
||||||
const canSubmitClone = canAddProject && cloneRemoteUrl.trim().length > 0;
|
const canSubmitClone = canAddProject && cloneRemoteUrl.trim().length > 0;
|
||||||
const highlightedRow = rows[highlightedIndex] ?? null;
|
const highlightedRow = rows[highlightedIndex] ?? null;
|
||||||
const hasHighlightedBrowseItem = Boolean(
|
const hasHighlightedBrowseItem = Boolean(
|
||||||
highlightedRow && (highlightedRow.type === 'up' || highlightedRow.type === 'directory')
|
highlightedRow && (highlightedRow.type === 'up' || highlightedRow.type === 'directory')
|
||||||
);
|
);
|
||||||
const submitModifierLabel = formatShortcutForDisplay('mod');
|
const submitModifierLabel = formatShortcutForDisplay('mod');
|
||||||
const submitActionLabel = isAlreadyAdded
|
const submitActionLabel = !isCloneMode && selectionPaths.length > 0
|
||||||
? t('directoryExplorerDialog.actions.alreadyAdded')
|
? t('directoryExplorerDialog.actions.addSelected')
|
||||||
: isCloneMode
|
: isAlreadyAdded
|
||||||
? isConfirming
|
? t('directoryExplorerDialog.actions.alreadyAdded')
|
||||||
? t('directoryExplorerDialog.actions.cloning')
|
: isCloneMode
|
||||||
: t('directoryExplorerDialog.actions.cloneAndAdd')
|
? isConfirming
|
||||||
: isConfirming
|
? t('directoryExplorerDialog.actions.cloning')
|
||||||
? t('directoryExplorerDialog.actions.adding')
|
: t('directoryExplorerDialog.actions.cloneAndAdd')
|
||||||
: shouldCreateTarget
|
: isConfirming
|
||||||
? t('directoryExplorerDialog.actions.createAndAdd')
|
? t('directoryExplorerDialog.actions.adding')
|
||||||
: t('directoryExplorerDialog.actions.addProject');
|
: shouldCreateTarget
|
||||||
|
? t('directoryExplorerDialog.actions.createAndAdd')
|
||||||
|
: t('directoryExplorerDialog.actions.addProject');
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const button = addButtonRef.current;
|
const button = addButtonRef.current;
|
||||||
@@ -429,14 +454,21 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
}, [addProject, addedProjectPaths, openProjectDraft, t]);
|
}, [addProject, addedProjectPaths, openProjectDraft, t]);
|
||||||
|
|
||||||
const finalizeSelection = React.useCallback(async (target: string) => {
|
const finalizeSelection = React.useCallback(async (target: string) => {
|
||||||
if (!target || isConfirming) return;
|
if (isConfirming) return;
|
||||||
const normalized = normalizeDirectoryPath(target);
|
const normalized = normalizeDirectoryPath(target);
|
||||||
if (normalized && addedProjectPaths.has(normalized)) return;
|
// Batch selections supersede the single-target flow. Only the single-target
|
||||||
|
// flow is blocked by an already-added (or missing) directory.
|
||||||
|
const selectionToAdd = isCloneMode
|
||||||
|
? []
|
||||||
|
: selectedPaths.filter((path) => {
|
||||||
|
const selectionNormalized = normalizeDirectoryPath(path);
|
||||||
|
return Boolean(selectionNormalized && !addedProjectPaths.has(selectionNormalized));
|
||||||
|
});
|
||||||
|
if (selectionToAdd.length === 0 && (!target || (normalized && addedProjectPaths.has(normalized)))) return;
|
||||||
let selectedTarget = target;
|
let selectedTarget = target;
|
||||||
|
|
||||||
setIsConfirming(true);
|
setIsConfirming(true);
|
||||||
try {
|
try {
|
||||||
const shouldCreateSelection = !isCloneMode && shouldCreateTarget && normalizeDirectoryPath(target) === normalizeDirectoryPath(targetPath);
|
|
||||||
if (isCloneMode) {
|
if (isCloneMode) {
|
||||||
const remoteUrl = cloneRemoteUrl.trim();
|
const remoteUrl = cloneRemoteUrl.trim();
|
||||||
if (!remoteUrl) {
|
if (!remoteUrl) {
|
||||||
@@ -451,6 +483,20 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
selectedTarget = result.path;
|
selectedTarget = result.path;
|
||||||
} else if (shouldCreateSelection) {
|
} else if (shouldCreateSelection) {
|
||||||
await opencodeClient.createDirectory(target, { asProject: true });
|
await opencodeClient.createDirectory(target, { asProject: true });
|
||||||
|
} else if (selectionToAdd.length > 0) {
|
||||||
|
const added = addProjects(selectionToAdd);
|
||||||
|
if (added.length === 0) {
|
||||||
|
toast.error(t('directoryExplorerDialog.toast.failedToAddProject'), {
|
||||||
|
description: t('directoryExplorerDialog.toast.selectValidDirectoryPath'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedPaths([]);
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
} else if (shouldCreateTarget && normalizeDirectoryPath(target) === normalizeDirectoryPath(targetPath)) {
|
||||||
|
await opencodeClient.createDirectory(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const project = await addProject(selectedTarget);
|
const project = await addProject(selectedTarget);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
@@ -467,7 +513,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
} finally {
|
} finally {
|
||||||
setIsConfirming(false);
|
setIsConfirming(false);
|
||||||
}
|
}
|
||||||
}, [addProject, addedProjectPaths, cloneRemoteUrl, isCloneMode, isConfirming, openProjectDraft, selectedGitIdentity?.id, shouldCreateTarget, targetPath, t]);
|
}, [addProject, addProjects, addedProjectPaths, cloneRemoteUrl, handleClose, isCloneMode, isConfirming, openProjectDraft, selectedGitIdentity?.id, selectedPaths, shouldCreateTarget, targetPath, t]);
|
||||||
|
|
||||||
const browseToDisplayPath = React.useCallback((displayPath: string) => {
|
const browseToDisplayPath = React.useCallback((displayPath: string) => {
|
||||||
setQuery(ensureBrowseDirectoryPath(displayPath));
|
setQuery(ensureBrowseDirectoryPath(displayPath));
|
||||||
@@ -529,6 +575,13 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
setHighlightedIndex((index) => Math.max(0, index - 1));
|
setHighlightedIndex((index) => Math.max(0, index - 1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (highlightedRow && highlightedRow.type === 'directory' && !highlightedRow.disabled) {
|
||||||
|
togglePathSelection(highlightedRow.path);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isPrimaryModifierPressed(event)) {
|
if (isPrimaryModifierPressed(event)) {
|
||||||
@@ -544,7 +597,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
}, [executeRow, finalizeSelection, handleClose, hasHighlightedBrowseItem, highlightedRow, query, rows.length, targetPath]);
|
}, [executeRow, finalizeSelection, handleClose, hasHighlightedBrowseItem, highlightedRow, query, rows.length, targetPath, togglePathSelection]);
|
||||||
|
|
||||||
const showHiddenToggle = (
|
const showHiddenToggle = (
|
||||||
<button
|
<button
|
||||||
@@ -684,15 +737,31 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
{t('directoryExplorerDialog.browse.addedBadge')}
|
{t('directoryExplorerDialog.browse.addedBadge')}
|
||||||
</span>
|
</span>
|
||||||
) : row.type === 'directory' ? (
|
) : row.type === 'directory' ? (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<button
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
type="button"
|
||||||
onClick={(event) => handleQuickAdd(event, row.path)}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
className="flex-shrink-0 rounded-full p-1 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
onClick={() => togglePathSelection(row.path)}
|
||||||
title={t('directoryExplorerDialog.browse.quickAdd')}
|
title={t('directoryExplorerDialog.browse.selectForAdd')}
|
||||||
>
|
aria-label={t('directoryExplorerDialog.browse.selectForAdd')}
|
||||||
<Icon name="add" className="h-3.5 w-3.5" />
|
aria-pressed={selectedPaths.includes(row.path)}
|
||||||
</button>
|
className="flex-shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={selectedPaths.includes(row.path) ? 'checkbox' : 'checkbox-blank'}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={(event) => handleQuickAdd(event, row.path)}
|
||||||
|
className="flex-shrink-0 rounded-full p-1 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||||
|
title={t('directoryExplorerDialog.browse.quickAdd')}
|
||||||
|
>
|
||||||
|
<Icon name="add" className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -738,7 +807,16 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
{isOpeningFinder ? t('directoryExplorerDialog.actions.openingFinder') : t('directoryExplorerDialog.actions.openInFinder')}
|
{isOpeningFinder ? t('directoryExplorerDialog.actions.openingFinder') : t('directoryExplorerDialog.actions.openInFinder')}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button variant="ghost" size="xs" onClick={() => setIsCloneMode((value) => !value)} disabled={isConfirming || isOpeningFinder} className={cn(isMobile && 'flex-1')}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCloneMode((value) => !value);
|
||||||
|
setSelectedPaths([]);
|
||||||
|
}}
|
||||||
|
disabled={isConfirming || isOpeningFinder}
|
||||||
|
className={cn(isMobile && 'flex-1')}
|
||||||
|
>
|
||||||
{isCloneMode ? t('directoryExplorerDialog.actions.addLocalProject') : t('directoryExplorerDialog.actions.cloneRepository')}
|
{isCloneMode ? t('directoryExplorerDialog.actions.addLocalProject') : t('directoryExplorerDialog.actions.cloneRepository')}
|
||||||
</Button>
|
</Button>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
|
|||||||
@@ -1612,6 +1612,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Im Finder öffnen',
|
'directoryExplorerDialog.actions.openInFinder': 'Im Finder öffnen',
|
||||||
'directoryExplorerDialog.actions.adding': 'Füge hinzu...',
|
'directoryExplorerDialog.actions.adding': 'Füge hinzu...',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Projekt hinzufügen',
|
'directoryExplorerDialog.actions.addProject': 'Projekt hinzufügen',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': 'Ausgewählte hinzufügen',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'Lokales Projekt hinzufügen',
|
'directoryExplorerDialog.actions.addLocalProject': 'Lokales Projekt hinzufügen',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': 'Repository klonen',
|
'directoryExplorerDialog.actions.cloneRepository': 'Repository klonen',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonen & hinzufügen',
|
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonen & hinzufügen',
|
||||||
@@ -1629,6 +1630,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': 'Übergeordnetes Verzeichnis',
|
'directoryExplorerDialog.browse.parentDirectory': 'Übergeordnetes Verzeichnis',
|
||||||
'directoryExplorerDialog.browse.addedBadge': 'Hinzugefügt',
|
'directoryExplorerDialog.browse.addedBadge': 'Hinzugefügt',
|
||||||
'directoryExplorerDialog.browse.quickAdd': 'Hinzufügen',
|
'directoryExplorerDialog.browse.quickAdd': 'Hinzufügen',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': 'Zum Hinzufügen auswählen',
|
||||||
'directoryExplorerDialog.footer.navigate': 'Navigieren',
|
'directoryExplorerDialog.footer.navigate': 'Navigieren',
|
||||||
'directoryExplorerDialog.footer.select': 'Auswählen',
|
'directoryExplorerDialog.footer.select': 'Auswählen',
|
||||||
'directoryExplorerDialog.footer.add': 'Hinzufügen',
|
'directoryExplorerDialog.footer.add': 'Hinzufügen',
|
||||||
|
|||||||
@@ -1813,6 +1813,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Open in Finder',
|
'directoryExplorerDialog.actions.openInFinder': 'Open in Finder',
|
||||||
'directoryExplorerDialog.actions.adding': 'Adding...',
|
'directoryExplorerDialog.actions.adding': 'Adding...',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Add project',
|
'directoryExplorerDialog.actions.addProject': 'Add project',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': 'Add selected',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'Add local project',
|
'directoryExplorerDialog.actions.addLocalProject': 'Add local project',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': 'Clone repository',
|
'directoryExplorerDialog.actions.cloneRepository': 'Clone repository',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Clone & add',
|
'directoryExplorerDialog.actions.cloneAndAdd': 'Clone & add',
|
||||||
@@ -1830,6 +1831,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': 'Parent directory',
|
'directoryExplorerDialog.browse.parentDirectory': 'Parent directory',
|
||||||
'directoryExplorerDialog.browse.addedBadge': 'Added',
|
'directoryExplorerDialog.browse.addedBadge': 'Added',
|
||||||
'directoryExplorerDialog.browse.quickAdd': 'Add',
|
'directoryExplorerDialog.browse.quickAdd': 'Add',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': 'Select for add',
|
||||||
'directoryExplorerDialog.footer.navigate': 'Navigate',
|
'directoryExplorerDialog.footer.navigate': 'Navigate',
|
||||||
'directoryExplorerDialog.footer.select': 'Select',
|
'directoryExplorerDialog.footer.select': 'Select',
|
||||||
'directoryExplorerDialog.footer.add': 'Add',
|
'directoryExplorerDialog.footer.add': 'Add',
|
||||||
|
|||||||
@@ -1791,6 +1791,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.actions.openInFinder": "Abrir en Finder",
|
"directoryExplorerDialog.actions.openInFinder": "Abrir en Finder",
|
||||||
"directoryExplorerDialog.actions.adding": "Añadiendo...",
|
"directoryExplorerDialog.actions.adding": "Añadiendo...",
|
||||||
"directoryExplorerDialog.actions.addProject": "Añadir proyecto",
|
"directoryExplorerDialog.actions.addProject": "Añadir proyecto",
|
||||||
|
"directoryExplorerDialog.actions.addSelected": "Añadir seleccionados",
|
||||||
"directoryExplorerDialog.actions.addLocalProject": "Añadir proyecto local",
|
"directoryExplorerDialog.actions.addLocalProject": "Añadir proyecto local",
|
||||||
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositorio",
|
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositorio",
|
||||||
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar y añadir",
|
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar y añadir",
|
||||||
@@ -1808,6 +1809,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.browse.parentDirectory": "Directorio padre",
|
"directoryExplorerDialog.browse.parentDirectory": "Directorio padre",
|
||||||
"directoryExplorerDialog.browse.addedBadge": "Añadido",
|
"directoryExplorerDialog.browse.addedBadge": "Añadido",
|
||||||
"directoryExplorerDialog.browse.quickAdd": "Añadir",
|
"directoryExplorerDialog.browse.quickAdd": "Añadir",
|
||||||
|
"directoryExplorerDialog.browse.selectForAdd": "Seleccionar para añadir",
|
||||||
"directoryExplorerDialog.footer.navigate": "Navegar",
|
"directoryExplorerDialog.footer.navigate": "Navegar",
|
||||||
"directoryExplorerDialog.footer.select": "Seleccionar",
|
"directoryExplorerDialog.footer.select": "Seleccionar",
|
||||||
"directoryExplorerDialog.footer.add": "Añadir",
|
"directoryExplorerDialog.footer.add": "Añadir",
|
||||||
|
|||||||
@@ -1571,6 +1571,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Ouvrir dans le Finder',
|
'directoryExplorerDialog.actions.openInFinder': 'Ouvrir dans le Finder',
|
||||||
'directoryExplorerDialog.actions.adding': 'Ajout...',
|
'directoryExplorerDialog.actions.adding': 'Ajout...',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Ajouter un projet',
|
'directoryExplorerDialog.actions.addProject': 'Ajouter un projet',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': 'Ajouter la sélection',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'Ajouter un projet local',
|
'directoryExplorerDialog.actions.addLocalProject': 'Ajouter un projet local',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': 'Cloner le dépôt',
|
'directoryExplorerDialog.actions.cloneRepository': 'Cloner le dépôt',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Cloner et ajouter',
|
'directoryExplorerDialog.actions.cloneAndAdd': 'Cloner et ajouter',
|
||||||
@@ -1588,6 +1589,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': 'Annuaire parent',
|
'directoryExplorerDialog.browse.parentDirectory': 'Annuaire parent',
|
||||||
'directoryExplorerDialog.browse.addedBadge': 'Ajouté',
|
'directoryExplorerDialog.browse.addedBadge': 'Ajouté',
|
||||||
'directoryExplorerDialog.browse.quickAdd': 'Ajouter',
|
'directoryExplorerDialog.browse.quickAdd': 'Ajouter',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': 'Sélectionner pour ajouter',
|
||||||
'directoryExplorerDialog.footer.navigate': 'Naviguer',
|
'directoryExplorerDialog.footer.navigate': 'Naviguer',
|
||||||
'directoryExplorerDialog.footer.select': 'Sélectionner',
|
'directoryExplorerDialog.footer.select': 'Sélectionner',
|
||||||
'directoryExplorerDialog.footer.add': 'Ajouter',
|
'directoryExplorerDialog.footer.add': 'Ajouter',
|
||||||
|
|||||||
@@ -1809,6 +1809,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Finderで開く',
|
'directoryExplorerDialog.actions.openInFinder': 'Finderで開く',
|
||||||
'directoryExplorerDialog.actions.adding': '追加中...',
|
'directoryExplorerDialog.actions.adding': '追加中...',
|
||||||
'directoryExplorerDialog.actions.addProject': 'プロジェクトを追加',
|
'directoryExplorerDialog.actions.addProject': 'プロジェクトを追加',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': '選択したものを追加',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'ローカルプロジェクトを追加',
|
'directoryExplorerDialog.actions.addLocalProject': 'ローカルプロジェクトを追加',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': 'リポジトリをクローン',
|
'directoryExplorerDialog.actions.cloneRepository': 'リポジトリをクローン',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': 'クローンして追加',
|
'directoryExplorerDialog.actions.cloneAndAdd': 'クローンして追加',
|
||||||
@@ -1826,6 +1827,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': '親ディレクトリ',
|
'directoryExplorerDialog.browse.parentDirectory': '親ディレクトリ',
|
||||||
'directoryExplorerDialog.browse.addedBadge': '追加済み',
|
'directoryExplorerDialog.browse.addedBadge': '追加済み',
|
||||||
'directoryExplorerDialog.browse.quickAdd': '追加',
|
'directoryExplorerDialog.browse.quickAdd': '追加',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': '追加するものを選択',
|
||||||
'directoryExplorerDialog.footer.navigate': '移動',
|
'directoryExplorerDialog.footer.navigate': '移動',
|
||||||
'directoryExplorerDialog.footer.select': '選択',
|
'directoryExplorerDialog.footer.select': '選択',
|
||||||
'directoryExplorerDialog.footer.add': '追加',
|
'directoryExplorerDialog.footer.add': '追加',
|
||||||
|
|||||||
@@ -1815,6 +1815,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Finder에서 열기',
|
'directoryExplorerDialog.actions.openInFinder': 'Finder에서 열기',
|
||||||
'directoryExplorerDialog.actions.adding': '추가 중…',
|
'directoryExplorerDialog.actions.adding': '추가 중…',
|
||||||
'directoryExplorerDialog.actions.addProject': '프로젝트 추가',
|
'directoryExplorerDialog.actions.addProject': '프로젝트 추가',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': '선택 항목 추가',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': '로컬 프로젝트 추가',
|
'directoryExplorerDialog.actions.addLocalProject': '로컬 프로젝트 추가',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': '저장소 복제',
|
'directoryExplorerDialog.actions.cloneRepository': '저장소 복제',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': '복제하고 추가',
|
'directoryExplorerDialog.actions.cloneAndAdd': '복제하고 추가',
|
||||||
@@ -1832,6 +1833,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': '상위 디렉터리',
|
'directoryExplorerDialog.browse.parentDirectory': '상위 디렉터리',
|
||||||
'directoryExplorerDialog.browse.addedBadge': '추가됨',
|
'directoryExplorerDialog.browse.addedBadge': '추가됨',
|
||||||
'directoryExplorerDialog.browse.quickAdd': '추가',
|
'directoryExplorerDialog.browse.quickAdd': '추가',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': '추가할 항목 선택',
|
||||||
'directoryExplorerDialog.footer.navigate': '탐색',
|
'directoryExplorerDialog.footer.navigate': '탐색',
|
||||||
'directoryExplorerDialog.footer.select': '선택',
|
'directoryExplorerDialog.footer.select': '선택',
|
||||||
'directoryExplorerDialog.footer.add': '추가',
|
'directoryExplorerDialog.footer.add': '추가',
|
||||||
|
|||||||
@@ -1880,6 +1880,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
||||||
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': 'Dodaj zaznaczone',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'Dodaj projekt lokalny',
|
'directoryExplorerDialog.actions.addLocalProject': 'Dodaj projekt lokalny',
|
||||||
'directoryExplorerDialog.actions.adding': 'Dodawanie...',
|
'directoryExplorerDialog.actions.adding': 'Dodawanie...',
|
||||||
'directoryExplorerDialog.actions.alreadyAdded': 'Już dodano',
|
'directoryExplorerDialog.actions.alreadyAdded': 'Już dodano',
|
||||||
@@ -1891,6 +1892,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.actions.openingFinder': 'Otwieranie...',
|
'directoryExplorerDialog.actions.openingFinder': 'Otwieranie...',
|
||||||
'directoryExplorerDialog.browse.addedBadge': 'Dodano',
|
'directoryExplorerDialog.browse.addedBadge': 'Dodano',
|
||||||
'directoryExplorerDialog.browse.quickAdd': 'Dodaj',
|
'directoryExplorerDialog.browse.quickAdd': 'Dodaj',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': 'Zaznacz do dodania',
|
||||||
'directoryExplorerDialog.browse.directories': 'Katalogi',
|
'directoryExplorerDialog.browse.directories': 'Katalogi',
|
||||||
'directoryExplorerDialog.browse.empty': 'Brak pasujących katalogów.',
|
'directoryExplorerDialog.browse.empty': 'Brak pasujących katalogów.',
|
||||||
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber potrzebuje dostępu do tego folderu.',
|
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber potrzebuje dostępu do tego folderu.',
|
||||||
|
|||||||
@@ -1791,6 +1791,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.actions.openInFinder": "Abrir no Finder",
|
"directoryExplorerDialog.actions.openInFinder": "Abrir no Finder",
|
||||||
"directoryExplorerDialog.actions.adding": "Adicionando...",
|
"directoryExplorerDialog.actions.adding": "Adicionando...",
|
||||||
"directoryExplorerDialog.actions.addProject": "Adicionar projeto",
|
"directoryExplorerDialog.actions.addProject": "Adicionar projeto",
|
||||||
|
"directoryExplorerDialog.actions.addSelected": "Adicionar selecionados",
|
||||||
"directoryExplorerDialog.actions.addLocalProject": "Adicionar projeto local",
|
"directoryExplorerDialog.actions.addLocalProject": "Adicionar projeto local",
|
||||||
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositório",
|
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositório",
|
||||||
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar e adicionar",
|
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar e adicionar",
|
||||||
@@ -1808,6 +1809,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.browse.parentDirectory": "Diretório pai",
|
"directoryExplorerDialog.browse.parentDirectory": "Diretório pai",
|
||||||
"directoryExplorerDialog.browse.addedBadge": "Adicionado",
|
"directoryExplorerDialog.browse.addedBadge": "Adicionado",
|
||||||
"directoryExplorerDialog.browse.quickAdd": "Adicionar",
|
"directoryExplorerDialog.browse.quickAdd": "Adicionar",
|
||||||
|
"directoryExplorerDialog.browse.selectForAdd": "Selecionar para adicionar",
|
||||||
"directoryExplorerDialog.footer.navigate": "Navegar",
|
"directoryExplorerDialog.footer.navigate": "Navegar",
|
||||||
"directoryExplorerDialog.footer.select": "Selecionar",
|
"directoryExplorerDialog.footer.select": "Selecionar",
|
||||||
"directoryExplorerDialog.footer.add": "Adicionar",
|
"directoryExplorerDialog.footer.add": "Adicionar",
|
||||||
|
|||||||
@@ -1791,6 +1791,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.actions.openInFinder": "Відкрити у Finder",
|
"directoryExplorerDialog.actions.openInFinder": "Відкрити у Finder",
|
||||||
"directoryExplorerDialog.actions.adding": "Додавання...",
|
"directoryExplorerDialog.actions.adding": "Додавання...",
|
||||||
"directoryExplorerDialog.actions.addProject": "Додати проєкт",
|
"directoryExplorerDialog.actions.addProject": "Додати проєкт",
|
||||||
|
"directoryExplorerDialog.actions.addSelected": "Додати вибрані",
|
||||||
"directoryExplorerDialog.actions.addLocalProject": "Додати локальний проєкт",
|
"directoryExplorerDialog.actions.addLocalProject": "Додати локальний проєкт",
|
||||||
"directoryExplorerDialog.actions.cloneRepository": "Клонувати репозиторій",
|
"directoryExplorerDialog.actions.cloneRepository": "Клонувати репозиторій",
|
||||||
"directoryExplorerDialog.actions.cloneAndAdd": "Клонувати й додати",
|
"directoryExplorerDialog.actions.cloneAndAdd": "Клонувати й додати",
|
||||||
@@ -1808,6 +1809,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.browse.parentDirectory": "Батьківський каталог",
|
"directoryExplorerDialog.browse.parentDirectory": "Батьківський каталог",
|
||||||
"directoryExplorerDialog.browse.addedBadge": "Додано",
|
"directoryExplorerDialog.browse.addedBadge": "Додано",
|
||||||
"directoryExplorerDialog.browse.quickAdd": "Додати",
|
"directoryExplorerDialog.browse.quickAdd": "Додати",
|
||||||
|
"directoryExplorerDialog.browse.selectForAdd": "Вибрати для додавання",
|
||||||
"directoryExplorerDialog.footer.navigate": "Навігація",
|
"directoryExplorerDialog.footer.navigate": "Навігація",
|
||||||
"directoryExplorerDialog.footer.select": "Вибрати",
|
"directoryExplorerDialog.footer.select": "Вибрати",
|
||||||
"directoryExplorerDialog.footer.add": "Додати",
|
"directoryExplorerDialog.footer.add": "Додати",
|
||||||
|
|||||||
@@ -1779,6 +1779,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中打开',
|
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中打开',
|
||||||
'directoryExplorerDialog.actions.adding': '添加中...',
|
'directoryExplorerDialog.actions.adding': '添加中...',
|
||||||
'directoryExplorerDialog.actions.addProject': '添加项目',
|
'directoryExplorerDialog.actions.addProject': '添加项目',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': '添加所选项目',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': '添加本地项目',
|
'directoryExplorerDialog.actions.addLocalProject': '添加本地项目',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': '克隆仓库',
|
'directoryExplorerDialog.actions.cloneRepository': '克隆仓库',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': '克隆并添加',
|
'directoryExplorerDialog.actions.cloneAndAdd': '克隆并添加',
|
||||||
@@ -1796,6 +1797,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': '上级目录',
|
'directoryExplorerDialog.browse.parentDirectory': '上级目录',
|
||||||
'directoryExplorerDialog.browse.addedBadge': '已添加',
|
'directoryExplorerDialog.browse.addedBadge': '已添加',
|
||||||
'directoryExplorerDialog.browse.quickAdd': '添加',
|
'directoryExplorerDialog.browse.quickAdd': '添加',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': '选择以添加',
|
||||||
'directoryExplorerDialog.footer.navigate': '导航',
|
'directoryExplorerDialog.footer.navigate': '导航',
|
||||||
'directoryExplorerDialog.footer.select': '选择',
|
'directoryExplorerDialog.footer.select': '选择',
|
||||||
'directoryExplorerDialog.footer.add': '添加',
|
'directoryExplorerDialog.footer.add': '添加',
|
||||||
|
|||||||
@@ -1783,6 +1783,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中開啟',
|
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中開啟',
|
||||||
'directoryExplorerDialog.actions.adding': '新增中...',
|
'directoryExplorerDialog.actions.adding': '新增中...',
|
||||||
'directoryExplorerDialog.actions.addProject': '新增專案',
|
'directoryExplorerDialog.actions.addProject': '新增專案',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': '新增所選項目',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': '新增本地專案',
|
'directoryExplorerDialog.actions.addLocalProject': '新增本地專案',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': '複製儲存庫',
|
'directoryExplorerDialog.actions.cloneRepository': '複製儲存庫',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': '複製並新增',
|
'directoryExplorerDialog.actions.cloneAndAdd': '複製並新增',
|
||||||
@@ -1800,6 +1801,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.browse.parentDirectory': '上層目錄',
|
'directoryExplorerDialog.browse.parentDirectory': '上層目錄',
|
||||||
'directoryExplorerDialog.browse.addedBadge': '已新增',
|
'directoryExplorerDialog.browse.addedBadge': '已新增',
|
||||||
'directoryExplorerDialog.browse.quickAdd': '添加',
|
'directoryExplorerDialog.browse.quickAdd': '添加',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': '選取以新增',
|
||||||
'directoryExplorerDialog.footer.navigate': '導覽',
|
'directoryExplorerDialog.footer.navigate': '導覽',
|
||||||
'directoryExplorerDialog.footer.select': '選擇',
|
'directoryExplorerDialog.footer.select': '選擇',
|
||||||
'directoryExplorerDialog.footer.add': '新增',
|
'directoryExplorerDialog.footer.add': '新增',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import type { ProjectEntry } from "@/lib/api/types"
|
import type { ProjectEntry } from "@/lib/api/types"
|
||||||
import type { DesktopSettings } from "@/lib/desktop"
|
import type { DesktopSettings } from "@/lib/desktop"
|
||||||
import { useProjectsStore } from "./useProjectsStore"
|
import { useProjectsStore } from "./useProjectsStore"
|
||||||
|
import { useDirectoryStore } from "./useDirectoryStore"
|
||||||
|
|
||||||
describe("useProjectsStore settings synchronization", () => {
|
describe("useProjectsStore settings synchronization", () => {
|
||||||
test("treats a successful empty project snapshot as authoritative", () => {
|
test("treats a successful empty project snapshot as authoritative", () => {
|
||||||
@@ -119,3 +120,53 @@ describe("useProjectsStore default model and thinking level", () => {
|
|||||||
expect(project?.defaultVariant).toBe(undefined)
|
expect(project?.defaultVariant).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("useProjectsStore.addProjects", () => {
|
||||||
|
const resetProjects = () => {
|
||||||
|
useProjectsStore.setState({
|
||||||
|
projects: [],
|
||||||
|
activeProjectId: null,
|
||||||
|
manualProjectOrder: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test("adds multiple new projects in one update and activates the first", () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = useProjectsStore.getState().addProjects(["/one", "/two", "/three"])
|
||||||
|
|
||||||
|
expect(added).toHaveLength(3)
|
||||||
|
expect(useProjectsStore.getState().projects.map((p) => p.path)).toEqual(["/one", "/two", "/three"])
|
||||||
|
expect(useProjectsStore.getState().activeProjectId).toBe(added[0].id)
|
||||||
|
expect(added[0].addedAt).toBe(added[1].addedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips already-added paths and duplicates within the batch", () => {
|
||||||
|
resetProjects()
|
||||||
|
useProjectsStore.getState().addProjects(["/one"])
|
||||||
|
|
||||||
|
const added = useProjectsStore.getState().addProjects(["/one", "/two", "/two", "/one"])
|
||||||
|
|
||||||
|
expect(added).toHaveLength(1)
|
||||||
|
expect(added[0].path).toBe("/two")
|
||||||
|
expect(useProjectsStore.getState().projects.map((p) => p.path)).toEqual(["/one", "/two"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips invalid paths and returns an empty array when nothing is addable", () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = useProjectsStore.getState().addProjects(["", " ", 42 as unknown as string])
|
||||||
|
|
||||||
|
expect(added).toEqual([])
|
||||||
|
expect(useProjectsStore.getState().projects).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("normalizes paths (trailing separators, backslashes, tilde expansion)", () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = useProjectsStore.getState().addProjects(["/repo/", "C:\\repo", "~/project"])
|
||||||
|
|
||||||
|
const home = useDirectoryStore.getState().homeDirectory;
|
||||||
|
expect(added.map((p) => p.path)).toEqual(["/repo", "C:/repo", home ? `${home}/project` : "~/project"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ interface ProjectsStore {
|
|||||||
activeProjectId: string | null;
|
activeProjectId: string | null;
|
||||||
manualProjectOrder: string[];
|
manualProjectOrder: string[];
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
||||||
|
=======
|
||||||
|
addProject: (path: string, options?: { label?: string; id?: string }) => ProjectEntry | null;
|
||||||
|
addProjects: (paths: string[]) => ProjectEntry[];
|
||||||
|
>>>>>>> e7506d349 (feat(projects): support adding multiple projects at once in the directory picker)
|
||||||
removeProject: (id: string) => void;
|
removeProject: (id: string) => void;
|
||||||
setActiveProject: (id: string) => void;
|
setActiveProject: (id: string) => void;
|
||||||
setActiveProjectIdOnly: (id: string) => void;
|
setActiveProjectIdOnly: (id: string) => void;
|
||||||
@@ -653,6 +658,55 @@ export const useProjectsStore = create<ProjectsStore>()(
|
|||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addProjects: (paths: string[]) => {
|
||||||
|
if (isVSCodeProjectsRuntime) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const current = get();
|
||||||
|
const existingPaths = new Set(current.projects.map((project) => project.path));
|
||||||
|
const now = Date.now();
|
||||||
|
const entries: ProjectEntry[] = [];
|
||||||
|
const seenPaths = new Set<string>();
|
||||||
|
|
||||||
|
for (const rawPath of paths) {
|
||||||
|
const validation = get().validateProjectPath(rawPath);
|
||||||
|
if (!validation.ok || !validation.normalizedPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedPath = validation.normalizedPath;
|
||||||
|
if (existingPaths.has(normalizedPath) || seenPaths.has(normalizedPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenPaths.add(normalizedPath);
|
||||||
|
entries.push({
|
||||||
|
id: createProjectIdFromPath(normalizedPath),
|
||||||
|
path: normalizedPath,
|
||||||
|
label: deriveProjectLabel(normalizedPath),
|
||||||
|
color: pickAutoColor([...current.projects, ...entries]),
|
||||||
|
addedAt: now,
|
||||||
|
lastOpenedAt: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextProjects = [...current.projects, ...entries];
|
||||||
|
set({ projects: nextProjects });
|
||||||
|
|
||||||
|
if (streamDebugEnabled()) {
|
||||||
|
console.info('[ProjectsStore] Added projects', entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror addProject: the first newly added project becomes active.
|
||||||
|
get().setActiveProject(entries[0].id);
|
||||||
|
for (const entry of entries) {
|
||||||
|
void get().discoverProjectIcon(entry.id);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
|
|
||||||
removeProject: (id: string) => {
|
removeProject: (id: string) => {
|
||||||
if (isVSCodeProjectsRuntime) {
|
if (isVSCodeProjectsRuntime) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user