feat(projects): add multiple projects at once in the directory picker (#2877)
feat(projects): add multiple projects at once in the directory picker
This commit is contained in:
@@ -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,9 +454,17 @@ 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);
|
||||||
@@ -449,6 +482,21 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
gitIdentityId: selectedGitIdentity?.id ?? null,
|
gitIdentityId: selectedGitIdentity?.id ?? null,
|
||||||
});
|
});
|
||||||
selectedTarget = result.path;
|
selectedTarget = result.path;
|
||||||
|
} else if (selectionToAdd.length > 0) {
|
||||||
|
// Batch path wins over single-target create: with checkboxes ticked,
|
||||||
|
// the user wants the selections added, not a fresh directory created
|
||||||
|
// for whatever happens to be typed in the filter.
|
||||||
|
const added = await addProjects(selectionToAdd);
|
||||||
|
if (added.length === 0) {
|
||||||
|
toast.error(t('directoryExplorerDialog.toast.failedToAddProject'), {
|
||||||
|
description: t('directoryExplorerDialog.toast.selectValidDirectoryPath'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(t('directoryExplorerDialog.toast.addedProjects', { count: added.length }));
|
||||||
|
setSelectedPaths([]);
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
} else if (shouldCreateSelection) {
|
} else if (shouldCreateSelection) {
|
||||||
await opencodeClient.createDirectory(target, { asProject: true });
|
await opencodeClient.createDirectory(target, { asProject: true });
|
||||||
}
|
}
|
||||||
@@ -467,7 +515,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));
|
||||||
@@ -508,6 +556,9 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear pending selections so the Finder-sourced target is honored
|
||||||
|
// instead of silently being absorbed by the batch branch.
|
||||||
|
setSelectedPaths([]);
|
||||||
await finalizeSelection(result.path);
|
await finalizeSelection(result.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('directoryExplorerDialog.toast.failedToSelectDirectory'), {
|
toast.error(t('directoryExplorerDialog.toast.failedToSelectDirectory'), {
|
||||||
@@ -529,6 +580,20 @@ 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 === ' ') {
|
||||||
|
// Only treat Space as a selection toggle when the user is actively
|
||||||
|
// browsing a directory (trailing slash or no filter typing). When
|
||||||
|
// the input is in path-entry mode, Space is a literal character
|
||||||
|
// and must reach the input value.
|
||||||
|
if (hasTrailingPathSeparator(query)) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (highlightedRow && highlightedRow.type === 'directory' && !highlightedRow.disabled) {
|
||||||
|
togglePathSelection(highlightedRow.path);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isPrimaryModifierPressed(event)) {
|
if (isPrimaryModifierPressed(event)) {
|
||||||
@@ -544,7 +609,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 +749,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={(event) => { event.stopPropagation(); 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 +819,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 ? (
|
||||||
|
|||||||
@@ -1624,6 +1624,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',
|
||||||
@@ -1641,6 +1642,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',
|
||||||
@@ -1649,6 +1651,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop hat den Zugriff auf das Verzeichnis verweigert.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop hat den Zugriff auf das Verzeichnis verweigert.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Fehler beim Öffnen des Verzeichnisses',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Fehler beim Öffnen des Verzeichnisses',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop konnte keinen Dateizugriff gewähren.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop konnte keinen Dateizugriff gewähren.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '{count} Projekt(e) hinzugefügt',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'Fehler beim Hinzufügen des Projekts',
|
'directoryExplorerDialog.toast.failedToAddProject': 'Fehler beim Hinzufügen des Projekts',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Geben Sie eine Repository-URL ein, bevor Sie klonen.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'Geben Sie eine Repository-URL ein, bevor Sie klonen.',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Bitte wählen Sie einen gültigen Verzeichnispfad aus.',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Bitte wählen Sie einen gültigen Verzeichnispfad aus.',
|
||||||
|
|||||||
@@ -1825,6 +1825,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',
|
||||||
@@ -1842,6 +1843,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',
|
||||||
@@ -1850,6 +1852,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop denied directory access.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop denied directory access.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Failed to open directory',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Failed to open directory',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop could not grant file access.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop could not grant file access.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': 'Added {count} project(s)',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'Failed to add project',
|
'directoryExplorerDialog.toast.failedToAddProject': 'Failed to add project',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Enter a repository URL before cloning.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'Enter a repository URL before cloning.',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Please select a valid directory path.',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Please select a valid directory path.',
|
||||||
|
|||||||
@@ -1803,6 +1803,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",
|
||||||
@@ -1820,6 +1821,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",
|
||||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "El escritorio denegó el acceso al directorio.",
|
"directoryExplorerDialog.toast.desktopDeniedAccess": "El escritorio denegó el acceso al directorio.",
|
||||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "No se pudo abrir el directorio",
|
"directoryExplorerDialog.toast.failedToOpenDirectory": "No se pudo abrir el directorio",
|
||||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "El escritorio no pudo otorgar acceso al archivo.",
|
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "El escritorio no pudo otorgar acceso al archivo.",
|
||||||
|
"directoryExplorerDialog.toast.addedProjects": "Se añadieron {count} proyecto(s)",
|
||||||
"directoryExplorerDialog.toast.failedToAddProject": "No se pudo añadir el proyecto",
|
"directoryExplorerDialog.toast.failedToAddProject": "No se pudo añadir el proyecto",
|
||||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Introduce una URL de repositorio antes de clonar.",
|
"directoryExplorerDialog.toast.cloneUrlRequired": "Introduce una URL de repositorio antes de clonar.",
|
||||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Por favor selecciona una ruta de directorio válida.",
|
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Por favor selecciona una ruta de directorio válida.",
|
||||||
|
|||||||
@@ -1582,6 +1582,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',
|
||||||
@@ -1599,6 +1600,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',
|
||||||
@@ -1607,6 +1609,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Le bureau a refusé l\'accès au répertoire.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Le bureau a refusé l\'accès au répertoire.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Échec de l\'ouverture du répertoire',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Échec de l\'ouverture du répertoire',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop n\'a pas pu accorder l\'accès aux fichiers.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop n\'a pas pu accorder l\'accès aux fichiers.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '{count} projet(s) ajouté(s)',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'Échec de l\'ajout du projet',
|
'directoryExplorerDialog.toast.failedToAddProject': 'Échec de l\'ajout du projet',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Entrez dans un dépôt URL avant le clonage.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'Entrez dans un dépôt URL avant le clonage.',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Veuillez sélectionner un chemin de répertoire valide.',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Veuillez sélectionner un chemin de répertoire valide.',
|
||||||
|
|||||||
@@ -1821,6 +1821,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': 'クローンして追加',
|
||||||
@@ -1838,6 +1839,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': '追加',
|
||||||
@@ -1846,6 +1848,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'デスクトップがディレクトリアクセスを拒否しました。',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'デスクトップがディレクトリアクセスを拒否しました。',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'ディレクトリを開けませんでした',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'ディレクトリを開けませんでした',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'デスクトップがファイルアクセスを許可できませんでした。',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'デスクトップがファイルアクセスを許可できませんでした。',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '{count}件のプロジェクトを追加しました',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'プロジェクトの追加に失敗しました',
|
'directoryExplorerDialog.toast.failedToAddProject': 'プロジェクトの追加に失敗しました',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'クローンする前にリポジトリURLを入力してください。',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'クローンする前にリポジトリURLを入力してください。',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '有効なディレクトリパスを選択してください。',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': '有効なディレクトリパスを選択してください。',
|
||||||
|
|||||||
@@ -1827,6 +1827,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': '복제하고 추가',
|
||||||
@@ -1844,6 +1845,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': '추가',
|
||||||
@@ -1852,6 +1854,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '데스크톱에서 디렉터리 접근이 거부되었습니다.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': '데스크톱에서 디렉터리 접근이 거부되었습니다.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '디렉터리를 열지 못했습니다',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': '디렉터리를 열지 못했습니다',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '데스크톱에서 파일 접근 권한을 부여하지 못했습니다.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '데스크톱에서 파일 접근 권한을 부여하지 못했습니다.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '프로젝트 {count}개 추가됨',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': '프로젝트 추가 실패',
|
'directoryExplorerDialog.toast.failedToAddProject': '프로젝트 추가 실패',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': '복제하기 전에 저장소 URL을 입력하세요.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': '복제하기 전에 저장소 URL을 입력하세요.',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '유효한 디렉터리 경로를 선택하세요.',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': '유효한 디렉터리 경로를 선택하세요.',
|
||||||
|
|||||||
@@ -1890,6 +1890,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',
|
||||||
@@ -1901,6 +1902,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.',
|
||||||
@@ -1919,6 +1921,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.title': 'Dodaj katalog projektu',
|
'directoryExplorerDialog.title': 'Dodaj katalog projektu',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Aplikacja desktopowa nie mogła przyznać dostępu do pliku.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Aplikacja desktopowa nie mogła przyznać dostępu do pliku.',
|
||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Aplikacja desktopowa odmówiła dostępu do katalogu.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Aplikacja desktopowa odmówiła dostępu do katalogu.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': 'Dodano {count} projektów',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'Nie udało się dodać projektu',
|
'directoryExplorerDialog.toast.failedToAddProject': 'Nie udało się dodać projektu',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Wpisz URL repozytorium przed klonowaniem.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'Wpisz URL repozytorium przed klonowaniem.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Nie udało się otworzyć katalogu',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Nie udało się otworzyć katalogu',
|
||||||
|
|||||||
@@ -1803,6 +1803,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",
|
||||||
@@ -1820,6 +1821,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",
|
||||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "O desktop negou o acesso ao diretório.",
|
"directoryExplorerDialog.toast.desktopDeniedAccess": "O desktop negou o acesso ao diretório.",
|
||||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "Não foi possível abrir o diretório",
|
"directoryExplorerDialog.toast.failedToOpenDirectory": "Não foi possível abrir o diretório",
|
||||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "O desktop não pôde conceder acesso ao arquivo.",
|
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "O desktop não pôde conceder acesso ao arquivo.",
|
||||||
|
"directoryExplorerDialog.toast.addedProjects": "Foram adicionados {count} projeto(s)",
|
||||||
"directoryExplorerDialog.toast.failedToAddProject": "Não foi possível adicionar o projeto",
|
"directoryExplorerDialog.toast.failedToAddProject": "Não foi possível adicionar o projeto",
|
||||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Insira uma URL de repositório antes de clonar.",
|
"directoryExplorerDialog.toast.cloneUrlRequired": "Insira uma URL de repositório antes de clonar.",
|
||||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Selecione um caminho de diretório válido.",
|
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Selecione um caminho de diretório válido.",
|
||||||
|
|||||||
@@ -1787,6 +1787,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.actions.openInFinder': 'Finder\'da aç',
|
'directoryExplorerDialog.actions.openInFinder': 'Finder\'da aç',
|
||||||
'directoryExplorerDialog.actions.adding': 'Ekleniyor...',
|
'directoryExplorerDialog.actions.adding': 'Ekleniyor...',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Proje ekle',
|
'directoryExplorerDialog.actions.addProject': 'Proje ekle',
|
||||||
|
'directoryExplorerDialog.actions.addSelected': 'Seçilenleri ekle',
|
||||||
'directoryExplorerDialog.actions.addLocalProject': 'Yerel proje ekle',
|
'directoryExplorerDialog.actions.addLocalProject': 'Yerel proje ekle',
|
||||||
'directoryExplorerDialog.actions.cloneRepository': 'Depoyu klonla',
|
'directoryExplorerDialog.actions.cloneRepository': 'Depoyu klonla',
|
||||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonla ve ekle',
|
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonla ve ekle',
|
||||||
@@ -1803,6 +1804,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.browse.retry': 'Yeniden dene',
|
'directoryExplorerDialog.browse.retry': 'Yeniden dene',
|
||||||
'directoryExplorerDialog.browse.parentDirectory': 'Üst dizin',
|
'directoryExplorerDialog.browse.parentDirectory': 'Üst dizin',
|
||||||
'directoryExplorerDialog.browse.addedBadge': 'Eklendi',
|
'directoryExplorerDialog.browse.addedBadge': 'Eklendi',
|
||||||
|
'directoryExplorerDialog.browse.selectForAdd': 'Eklemek için seç',
|
||||||
'directoryExplorerDialog.browse.quickAdd': 'Ekle',
|
'directoryExplorerDialog.browse.quickAdd': 'Ekle',
|
||||||
'directoryExplorerDialog.footer.navigate': 'Gezin',
|
'directoryExplorerDialog.footer.navigate': 'Gezin',
|
||||||
'directoryExplorerDialog.footer.select': 'Seç',
|
'directoryExplorerDialog.footer.select': 'Seç',
|
||||||
@@ -1812,6 +1814,7 @@ export const dict = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Masaüstü uygulaması dizin erişimini reddetti.',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Masaüstü uygulaması dizin erişimini reddetti.',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Dizin açılamadı',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Dizin açılamadı',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Masaüstü uygulaması dosya erişimi veremedi.',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Masaüstü uygulaması dosya erişimi veremedi.',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '{count} proje eklendi',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': 'Proje eklenemedi',
|
'directoryExplorerDialog.toast.failedToAddProject': 'Proje eklenemedi',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Klonlamadan önce bir depo URL\'si girin.',
|
'directoryExplorerDialog.toast.cloneUrlRequired': 'Klonlamadan önce bir depo URL\'si girin.',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Geçerli bir dizin yolu seçin.',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Geçerli bir dizin yolu seçin.',
|
||||||
|
|||||||
@@ -1803,6 +1803,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": "Клонувати й додати",
|
||||||
@@ -1820,6 +1821,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": "Додати",
|
||||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "Десктопному застосунку заборонено доступ до каталогу.",
|
"directoryExplorerDialog.toast.desktopDeniedAccess": "Десктопному застосунку заборонено доступ до каталогу.",
|
||||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "Не вдалося відкрити каталог",
|
"directoryExplorerDialog.toast.failedToOpenDirectory": "Не вдалося відкрити каталог",
|
||||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "Десктопний застосунок не зміг надати доступ до файлу.",
|
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "Десктопний застосунок не зміг надати доступ до файлу.",
|
||||||
|
"directoryExplorerDialog.toast.addedProjects": "Додано {count} проєкт(и)",
|
||||||
"directoryExplorerDialog.toast.failedToAddProject": "Не вдалося додати проєкт",
|
"directoryExplorerDialog.toast.failedToAddProject": "Не вдалося додати проєкт",
|
||||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Введіть URL репозиторію перед клонуванням.",
|
"directoryExplorerDialog.toast.cloneUrlRequired": "Введіть URL репозиторію перед клонуванням.",
|
||||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Виберіть правильний шлях до каталогу.",
|
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Виберіть правильний шлях до каталогу.",
|
||||||
|
|||||||
@@ -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': '添加',
|
||||||
@@ -1816,6 +1818,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒绝了目录访问。',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒绝了目录访问。',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '打开目录失败',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': '打开目录失败',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端无法授予文件访问权限。',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端无法授予文件访问权限。',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '已添加 {count} 个项目',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': '添加项目失败',
|
'directoryExplorerDialog.toast.failedToAddProject': '添加项目失败',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': '克隆前请输入仓库 URL。',
|
'directoryExplorerDialog.toast.cloneUrlRequired': '克隆前请输入仓库 URL。',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '请选择有效的目录路径。',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': '请选择有效的目录路径。',
|
||||||
|
|||||||
@@ -1795,6 +1795,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': '複製並新增',
|
||||||
@@ -1812,6 +1813,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': '新增',
|
||||||
@@ -1820,6 +1822,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒絕了目錄存取。',
|
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒絕了目錄存取。',
|
||||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '開啟目錄失敗',
|
'directoryExplorerDialog.toast.failedToOpenDirectory': '開啟目錄失敗',
|
||||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端無法授予檔案存取權限。',
|
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端無法授予檔案存取權限。',
|
||||||
|
'directoryExplorerDialog.toast.addedProjects': '已新增 {count} 個專案',
|
||||||
'directoryExplorerDialog.toast.failedToAddProject': '新增專案失敗',
|
'directoryExplorerDialog.toast.failedToAddProject': '新增專案失敗',
|
||||||
'directoryExplorerDialog.toast.cloneUrlRequired': '複製前請輸入儲存庫 URL。',
|
'directoryExplorerDialog.toast.cloneUrlRequired': '複製前請輸入儲存庫 URL。',
|
||||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '請選擇有效的目錄路徑。',
|
'directoryExplorerDialog.toast.selectValidDirectoryPath': '請選擇有效的目錄路徑。',
|
||||||
|
|||||||
@@ -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", async () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = await 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", async () => {
|
||||||
|
resetProjects()
|
||||||
|
await useProjectsStore.getState().addProjects(["/one"])
|
||||||
|
|
||||||
|
const added = await 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", async () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = await useProjectsStore.getState().addProjects(["", " ", 42 as unknown as string])
|
||||||
|
|
||||||
|
expect(added).toEqual([])
|
||||||
|
expect(useProjectsStore.getState().projects).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("normalizes paths (trailing separators, backslashes, tilde expansion)", async () => {
|
||||||
|
resetProjects()
|
||||||
|
|
||||||
|
const added = await 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"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface ProjectsStore {
|
|||||||
manualProjectOrder: string[];
|
manualProjectOrder: string[];
|
||||||
|
|
||||||
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
||||||
|
addProjects: (paths: string[]) => Promise<ProjectEntry[]>;
|
||||||
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 +654,69 @@ export const useProjectsStore = create<ProjectsStore>()(
|
|||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addProjects: async (paths: string[]) => {
|
||||||
|
if (isVSCodeProjectsRuntime) {
|
||||||
|
// VS Code paths are added via runtimeApis.vscode.addWorkspaceFolder,
|
||||||
|
// which is reached only by addProject. Iterate so valid selections
|
||||||
|
// succeed instead of silently returning []. Dedupe by path so the
|
||||||
|
// returned array mirrors the non-VS Code contract.
|
||||||
|
const added: ProjectEntry[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const path of paths) {
|
||||||
|
if (seen.has(path)) continue;
|
||||||
|
seen.add(path);
|
||||||
|
const project = await get().addProject(path);
|
||||||
|
if (project) {
|
||||||
|
added.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -133,4 +133,44 @@ describe('issue #2582: addProject in the VS Code runtime', () => {
|
|||||||
expect(added).toBeNull();
|
expect(added).toBeNull();
|
||||||
expect(useProjectsStore.getState().projects.find((p) => p.path === '/other/path')).toBeFalsy();
|
expect(useProjectsStore.getState().projects.find((p) => p.path === '/other/path')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('addProjects iterates addProject in the VS Code runtime so valid selections succeed', async () => {
|
||||||
|
// Regression: addProjects used to return [] unconditionally for the
|
||||||
|
// VS Code runtime, which made the batch-add path toast "Failed to
|
||||||
|
// add project" even for valid selections. The fix calls
|
||||||
|
// addWorkspaceFolder per path; we assert the host is invoked once
|
||||||
|
// per selection (not skipped) and that any successful add returns
|
||||||
|
// a non-null entry.
|
||||||
|
const added = await useProjectsStore.getState().addProjects([
|
||||||
|
'/home/user/project-a',
|
||||||
|
'/home/user/project-b',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(addWorkspaceFolderCalls).toEqual([
|
||||||
|
'/home/user/project-a',
|
||||||
|
'/home/user/project-b',
|
||||||
|
]);
|
||||||
|
// The mock's addWorkspaceFolder returns the second entry keyed by
|
||||||
|
// `path`, so project-a lands; project-b is not reflected in
|
||||||
|
// projects because the mock's hardcoded return array doesn't
|
||||||
|
// include it. The point of the test is the call sequence, not the
|
||||||
|
// final projects state (covered by the dedicated addProject tests).
|
||||||
|
expect(added.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addProjects dedupes paths within a single batch in the VS Code runtime', async () => {
|
||||||
|
// A path repeated within one batch must hit the extension host once,
|
||||||
|
// not twice — mirrors the non-VS Code contract (seenPaths Set).
|
||||||
|
addWorkspaceFolderCalls.length = 0;
|
||||||
|
await useProjectsStore.getState().addProjects([
|
||||||
|
'/home/user/project-a',
|
||||||
|
'/home/user/project-a',
|
||||||
|
'/home/user/project-b',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(addWorkspaceFolderCalls).toEqual([
|
||||||
|
'/home/user/project-a',
|
||||||
|
'/home/user/project-b',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user