Add a keyboard-driven project setup flow (#2097)

* feat(projects): add keyboard-driven project setup

* test(app): update add project loading assertion

* fix(projects): harden GitHub project setup
This commit is contained in:
Mohamed Boudra
2026-07-15 21:16:10 +02:00
committed by GitHub
parent 90e0a0e353
commit dfe3330ef8
52 changed files with 3851 additions and 1023 deletions
@@ -0,0 +1,182 @@
import { describe, expect, it } from "vitest";
import {
backAddProjectPage,
chooseAddProjectHost,
currentAddProjectPage,
moveAddProjectActiveIndex,
moveAddProjectSelection,
openAddProjectFlow,
openDirectorySearchPage,
openGithubLocationPage,
openNewDirectoryNamePage,
openNewDirectoryParentPage,
setAddProjectActiveIndex,
setAddProjectPageInput,
setNewDirectoryName,
type AddProjectHost,
} from "./model";
import {
buildAddProjectMethods,
buildCloneLocationOptions,
buildManualGithubRepositoryChoices,
} from "./options";
const HOST: AddProjectHost = {
serverId: "host-1",
label: "Local",
canAddProject: true,
canBrowse: true,
canCloneGithubRepositories: true,
canSearchGithubRepositories: true,
canCreateDirectory: true,
};
describe("Add Project navigation", () => {
it("skips a single connected host without adding it to history", () => {
const state = openAddProjectFlow({ hosts: [HOST] });
expect(currentAddProjectPage(state)).toEqual({
kind: "method",
hostId: "host-1",
query: "",
activeIndex: 0,
error: null,
});
expect(backAddProjectPage(state)).toBeNull();
});
it("restores page input and selection after Back", () => {
const secondHost = { ...HOST, serverId: "host-2", label: "Remote" };
let state = openAddProjectFlow({ hosts: [HOST, secondHost] });
state = setAddProjectPageInput(state, "rem");
state = setAddProjectActiveIndex(state, 1);
state = chooseAddProjectHost(state, secondHost.serverId);
state = openDirectorySearchPage(state, secondHost.serverId);
state = backAddProjectPage(state) ?? state;
state = backAddProjectPage(state) ?? state;
expect(currentAddProjectPage(state)).toEqual({
kind: "host",
query: "rem",
activeIndex: 1,
error: null,
});
});
it("wraps keyboard selection in both directions", () => {
expect(moveAddProjectActiveIndex(2, 3, "next")).toBe(0);
expect(moveAddProjectActiveIndex(0, 3, "previous")).toBe(2);
expect(moveAddProjectSelection(0, [true, false, true], "next")).toBe(2);
});
it("restores a directory name after returning to and reselecting its parent", () => {
let state = openAddProjectFlow({ hosts: [HOST] });
state = openNewDirectoryParentPage(state, HOST.serverId);
state = openNewDirectoryNamePage(state, HOST.serverId, "~/dev");
state = setNewDirectoryName(state, "command-center");
state = backAddProjectPage(state) ?? state;
state = openNewDirectoryNamePage(state, HOST.serverId, "~/dev");
expect(currentAddProjectPage(state)).toMatchObject({
kind: "new-directory-name",
parentPath: "~/dev",
name: "command-center",
});
});
it("restores the GitHub destination query and active parent when reopening a repository", () => {
const repository = {
id: "repo-1",
nameWithOwner: "getpaseo/paseo",
cloneUrl: "git@github.com:getpaseo/paseo.git",
description: null,
visibility: "public",
updatedAt: null,
};
let state = openAddProjectFlow({ hosts: [HOST] });
state = openGithubLocationPage(state, HOST.serverId, repository);
state = setAddProjectPageInput(state, "~/dev");
state = setAddProjectActiveIndex(state, 2);
state = backAddProjectPage(state) ?? state;
state = openGithubLocationPage(state, HOST.serverId, repository);
expect(currentAddProjectPage(state)).toMatchObject({
kind: "github-location",
query: "~/dev",
activeIndex: 2,
});
});
});
describe("Add Project options", () => {
it("keeps host-upgrade methods discoverable while hiding local-only Browse", () => {
expect(
buildAddProjectMethods({
...HOST,
canBrowse: false,
canCloneGithubRepositories: false,
canSearchGithubRepositories: false,
canCreateDirectory: false,
}),
).toEqual([
{
id: "directory-search",
label: "Search for directory",
description: "Find a directory on Local",
},
{
id: "github",
label: "Clone from GitHub",
description: "Update this host to clone GitHub repositories",
disabled: true,
},
{
id: "new-directory",
label: "New directory",
description: "Update this host to create directories",
disabled: true,
},
]);
});
it("offers manual URL and protocol-specific owner/repo clone choices", () => {
expect(buildManualGithubRepositoryChoices("git@github.com:getpaseo/paseo.git")).toEqual([
expect.objectContaining({
id: "manual:git@github.com:getpaseo/paseo.git",
nameWithOwner: "getpaseo/paseo",
cloneUrl: "git@github.com:getpaseo/paseo.git",
}),
]);
expect(buildManualGithubRepositoryChoices("getpaseo/paseo")).toEqual([
expect.objectContaining({ cloneProtocol: "https", cloneUrl: "getpaseo/paseo" }),
expect.objectContaining({ cloneProtocol: "ssh", cloneUrl: "getpaseo/paseo" }),
]);
expect(buildManualGithubRepositoryChoices("paseo")).toEqual([]);
});
it("shows final clone paths while retaining parent paths as values", () => {
expect(
buildCloneLocationOptions({
parents: ["~/dev", "~/workspace"],
repositoryName: "paseo",
existingPaths: ["~/workspace/paseo"],
}),
).toEqual([
{
id: "~/dev",
path: "~/dev",
displayPath: "~/dev/paseo",
secondaryText: "Parent directory: ~/dev",
disabled: false,
},
{
id: "~/workspace",
path: "~/workspace",
displayPath: "~/workspace/paseo",
secondaryText: "Already exists",
disabled: true,
},
]);
});
});