fix(directory-explorer): make batch add work in VS Code; add Turkish locale keys
Two non-blockers from the openchamber-bot review at b2ab4af15:
1. VS Code batch add reported a misleading failure. addProjects
returned [] unconditionally for the VS Code runtime because
addWorkspaceFolder is reached only by addProject. Iterate addProject
per path so valid selections succeed and the host is called once
per selection. addProjects is now async (returns Promise<ProjectEntry[]>);
the call site in DirectoryExplorerDialog awaits it; existing
tests in useProjectsStore.test.ts updated to await.
2. Turkish locale (tr.ts) was missing the two new keys that other
11 dictionaries received: actions.addSelected and browse.selectForAdd.
Add both with real Turkish translations: "Seçilenleri ekle" and
"Eklemek için seç".
This commit is contained in:
@@ -486,7 +486,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
// 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 = addProjects(selectionToAdd);
|
||||
const added = await addProjects(selectionToAdd);
|
||||
if (added.length === 0) {
|
||||
toast.error(t('directoryExplorerDialog.toast.failedToAddProject'), {
|
||||
description: t('directoryExplorerDialog.toast.selectValidDirectoryPath'),
|
||||
|
||||
@@ -1775,6 +1775,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Finder\'da aç',
|
||||
'directoryExplorerDialog.actions.adding': 'Ekleniyor...',
|
||||
'directoryExplorerDialog.actions.addProject': 'Proje ekle',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Seçilenleri ekle',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Yerel proje ekle',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'Depoyu klonla',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonla ve ekle',
|
||||
@@ -1791,6 +1792,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.browse.retry': 'Yeniden dene',
|
||||
'directoryExplorerDialog.browse.parentDirectory': 'Üst dizin',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Eklendi',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Eklemek için seç',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Ekle',
|
||||
'directoryExplorerDialog.footer.navigate': 'Gezin',
|
||||
'directoryExplorerDialog.footer.select': 'Seç',
|
||||
|
||||
@@ -130,10 +130,10 @@ describe("useProjectsStore.addProjects", () => {
|
||||
})
|
||||
}
|
||||
|
||||
test("adds multiple new projects in one update and activates the first", () => {
|
||||
test("adds multiple new projects in one update and activates the first", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = useProjectsStore.getState().addProjects(["/one", "/two", "/three"])
|
||||
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"])
|
||||
@@ -141,30 +141,30 @@ describe("useProjectsStore.addProjects", () => {
|
||||
expect(added[0].addedAt).toBe(added[1].addedAt)
|
||||
})
|
||||
|
||||
test("skips already-added paths and duplicates within the batch", () => {
|
||||
test("skips already-added paths and duplicates within the batch", async () => {
|
||||
resetProjects()
|
||||
useProjectsStore.getState().addProjects(["/one"])
|
||||
await useProjectsStore.getState().addProjects(["/one"])
|
||||
|
||||
const added = useProjectsStore.getState().addProjects(["/one", "/two", "/two", "/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", () => {
|
||||
test("skips invalid paths and returns an empty array when nothing is addable", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = useProjectsStore.getState().addProjects(["", " ", 42 as unknown as string])
|
||||
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)", () => {
|
||||
test("normalizes paths (trailing separators, backslashes, tilde expansion)", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = useProjectsStore.getState().addProjects(["/repo/", "C:\\repo", "~/project"])
|
||||
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,7 +50,7 @@ interface ProjectsStore {
|
||||
manualProjectOrder: string[];
|
||||
|
||||
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
||||
addProjects: (paths: string[]) => ProjectEntry[];
|
||||
addProjects: (paths: string[]) => Promise<ProjectEntry[]>;
|
||||
removeProject: (id: string) => void;
|
||||
setActiveProject: (id: string) => void;
|
||||
setActiveProjectIdOnly: (id: string) => void;
|
||||
@@ -654,9 +654,19 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
return entry;
|
||||
},
|
||||
|
||||
addProjects: (paths: string[]) => {
|
||||
addProjects: async (paths: string[]) => {
|
||||
if (isVSCodeProjectsRuntime) {
|
||||
return [];
|
||||
// VS Code paths are added via runtimeApis.vscode.addWorkspaceFolder,
|
||||
// which is reached only by addProject. Iterate so valid selections
|
||||
// succeed instead of silently returning [].
|
||||
const added: ProjectEntry[] = [];
|
||||
for (const path of paths) {
|
||||
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));
|
||||
|
||||
@@ -133,4 +133,28 @@ describe('issue #2582: addProject in the VS Code runtime', () => {
|
||||
expect(added).toBeNull();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user