Keep New Workspace drafts when archiving a workspace (#1838)
* fix(app): preserve new workspace drafts across archive The New Workspace project picker was still tied to the last active workspace. Archiving that workspace could make the picker fall back to another project, changing the draft key and clearing the composer. * refactor(app): extract new workspace project selection Keep the archive-draft fix out of the screen effect by moving the project selection transition into a small pure model with focused coverage. * test(app): seed host in archive draft regression Make the new Playwright regression self-contained by seeding the local host before opening the app shell. * refactor(app): home new workspace project picker state * fix(app): reset project picker on capability hydration * fix(app): preserve manual project picks during hydration * fix(app): keep project selection through archive gaps * fix(app): narrow archive-gap project preservation * fix(app): refresh new workspace project selection
This commit is contained in:
@@ -56,12 +56,9 @@ import { toErrorMessage } from "@/utils/error-messages";
|
||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
import {
|
||||
filterWorkspaceProjectsForHost,
|
||||
getHostProjectSourceDirectory,
|
||||
hostProjectFromRoute,
|
||||
hostProjectFromWorkspace,
|
||||
resolveInitialWorkspaceProject,
|
||||
resolveSelectedHostProject,
|
||||
useHostProjects,
|
||||
type HostProjectListItem,
|
||||
} from "@/projects/host-projects";
|
||||
@@ -88,6 +85,7 @@ import {
|
||||
resolveNewWorkspaceAutomaticServerId,
|
||||
resolveNewWorkspaceInitialServerId,
|
||||
} from "./new-workspace-initial-context";
|
||||
import { useNewWorkspaceProjectPicker } from "./new-workspace/project-picker";
|
||||
|
||||
function resolveCheckoutRequest(
|
||||
selectedItem: PickerItem | null,
|
||||
@@ -168,7 +166,6 @@ interface PickerSelection {
|
||||
|
||||
const BRANCH_OPTION_PREFIX = "branch:";
|
||||
const PR_OPTION_PREFIX = "github-pr:";
|
||||
const PROJECT_OPTION_PREFIX = "project:";
|
||||
const PROJECT_ICON_FALLBACK_FONT_SIZE = 10;
|
||||
// Height of a single picker-trigger badge. The Base-row spacer reserves exactly
|
||||
// this so toggling Isolation to Local hides the row without shifting the form.
|
||||
@@ -510,20 +507,6 @@ function prOptionId(number: number): string {
|
||||
return `${PR_OPTION_PREFIX}${number}`;
|
||||
}
|
||||
|
||||
function projectOptionId(projectId: string): string {
|
||||
return `${PROJECT_OPTION_PREFIX}${projectId}`;
|
||||
}
|
||||
|
||||
function computeProjectOptionData(projects: readonly HostProjectListItem[]) {
|
||||
const projectByOptionId = new Map<string, HostProjectListItem>();
|
||||
const options = projects.map((project) => {
|
||||
const id = projectOptionId(project.projectKey);
|
||||
projectByOptionId.set(id, project);
|
||||
return { id, label: project.projectName };
|
||||
});
|
||||
return { options, projectByOptionId };
|
||||
}
|
||||
|
||||
function NewWorkspacePickerOption({
|
||||
option,
|
||||
selected,
|
||||
@@ -1287,7 +1270,6 @@ interface NewWorkspaceInitialContextState {
|
||||
projects: HostProjectListItem[];
|
||||
routeProject: HostProjectListItem | null;
|
||||
lastActiveProject: HostProjectListItem | null;
|
||||
routeDisplayName: string;
|
||||
}
|
||||
|
||||
function useNewWorkspaceInitialContext({
|
||||
@@ -1354,112 +1336,6 @@ function useNewWorkspaceInitialContext({
|
||||
projects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
routeDisplayName,
|
||||
};
|
||||
}
|
||||
|
||||
interface NewWorkspaceProjectPickerInput {
|
||||
selectedServerId: string;
|
||||
projects: HostProjectListItem[];
|
||||
routeProject: HostProjectListItem | null;
|
||||
lastActiveProject: HostProjectListItem | null;
|
||||
displayName?: string;
|
||||
allowAllProjects: boolean;
|
||||
}
|
||||
|
||||
interface NewWorkspaceProjectPickerState {
|
||||
projects: HostProjectListItem[];
|
||||
selectedProject: HostProjectListItem | null;
|
||||
selectedSourceDirectory: string | null;
|
||||
selectedDisplayName: string;
|
||||
projectPickerOptions: Array<{ id: string; label: string }>;
|
||||
projectByOptionId: Map<string, HostProjectListItem>;
|
||||
selectedProjectOptionId: string;
|
||||
projectTriggerLabel: string;
|
||||
handleSelectProjectOption: (id: string) => void;
|
||||
}
|
||||
|
||||
function useNewWorkspaceProjectPicker({
|
||||
selectedServerId,
|
||||
projects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
displayName: displayNameProp,
|
||||
allowAllProjects,
|
||||
}: NewWorkspaceProjectPickerInput): NewWorkspaceProjectPickerState {
|
||||
const [manualProjectKey, setManualProjectKey] = useState<string | null>(null);
|
||||
const displayName = displayNameProp?.trim() ?? "";
|
||||
const selectableProjects = useMemo(
|
||||
() =>
|
||||
filterWorkspaceProjectsForHost({ projects, serverId: selectedServerId, allowAllProjects }),
|
||||
[allowAllProjects, projects, selectedServerId],
|
||||
);
|
||||
const initialProject = useMemo(
|
||||
() =>
|
||||
resolveInitialWorkspaceProject({
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
projects: selectableProjects,
|
||||
serverId: selectedServerId,
|
||||
allowAllProjects,
|
||||
}),
|
||||
[allowAllProjects, lastActiveProject, routeProject, selectableProjects, selectedServerId],
|
||||
);
|
||||
|
||||
const routeProjectKey = routeProject?.projectKey ?? null;
|
||||
useEffect(() => {
|
||||
setManualProjectKey(null);
|
||||
}, [routeProjectKey]);
|
||||
|
||||
const selectedProjectKey = useMemo(() => {
|
||||
if (manualProjectKey) {
|
||||
const manual = resolveSelectedHostProject({
|
||||
selectedProjectKey: manualProjectKey,
|
||||
projects: selectableProjects,
|
||||
routeProject: null,
|
||||
lastActiveProject: null,
|
||||
});
|
||||
if (manual) return manual.projectKey;
|
||||
}
|
||||
return initialProject?.projectKey ?? null;
|
||||
}, [initialProject, manualProjectKey, selectableProjects]);
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() =>
|
||||
resolveSelectedHostProject({
|
||||
selectedProjectKey,
|
||||
projects: selectableProjects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
}),
|
||||
[lastActiveProject, routeProject, selectableProjects, selectedProjectKey],
|
||||
);
|
||||
const { options: projectPickerOptions, projectByOptionId } = useMemo(
|
||||
() => computeProjectOptionData(selectableProjects),
|
||||
[selectableProjects],
|
||||
);
|
||||
const handleSelectProjectOption = useCallback(
|
||||
(id: string) => {
|
||||
const project = projectByOptionId.get(id);
|
||||
if (!project) return;
|
||||
if (!allowAllProjects && !project.hosts.some((host) => host.canCreateWorktree)) return;
|
||||
setManualProjectKey(project.projectKey);
|
||||
},
|
||||
[allowAllProjects, projectByOptionId],
|
||||
);
|
||||
|
||||
return {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSourceDirectory: selectedProject
|
||||
? getHostProjectSourceDirectory(selectedProject, selectedServerId)
|
||||
: null,
|
||||
selectedDisplayName: selectedProject?.projectName ?? displayName,
|
||||
projectPickerOptions,
|
||||
projectByOptionId,
|
||||
selectedProjectOptionId: selectedProject ? projectOptionId(selectedProject.projectKey) : "",
|
||||
projectTriggerLabel: selectedProject?.projectName ?? "Choose project",
|
||||
handleSelectProjectOption,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1698,7 +1574,6 @@ export function NewWorkspaceScreen({
|
||||
projects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
routeDisplayName,
|
||||
} = useNewWorkspaceInitialContext({
|
||||
serverId,
|
||||
sourceDirectory: sourceDirectoryProp,
|
||||
@@ -1747,7 +1622,6 @@ export function NewWorkspaceScreen({
|
||||
projects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
displayName: routeDisplayName,
|
||||
allowAllProjects: supportsWorkspaceMultiplicity,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user