Repository navigation
fix(mobile): adding a folder that is already a project opens it #34942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
13362cb
5558f6b
3a7ec04
7b53100
5274434
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -387,7 +387,26 @@ function useBrowsePathInput( | |||||||||||||||||||||||||||||||
| [browseNavigation], | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return { isBrowseNavigating, pathInput, setPathInput, navigateToBrowsePath }; | ||||||||||||||||||||||||||||||||
| // Registered roots are absolute and `~` expands on the host, so ask the host for its home. | ||||||||||||||||||||||||||||||||
| const resolveHomePath = async (path: string): Promise<string> => { | ||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||
| !/^~(?:[\\/]|$)/.test(path) || | ||||||||||||||||||||||||||||||||
| !environment || | ||||||||||||||||||||||||||||||||
| !readEnvironmentScope(environment.environmentId, AuthFilesystemReadScope) || | ||||||||||||||||||||||||||||||||
| !canPreloadBrowsePath(environmentRuntime?.connectionState) | ||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||
| return path; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const home = await loadBrowsePath({ | ||||||||||||||||||||||||||||||||
| environmentId: environment.environmentId, | ||||||||||||||||||||||||||||||||
| input: { partialPath: "~" }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| return AsyncResult.isSuccess(home) && home.value | ||||||||||||||||||||||||||||||||
| ? `${home.value.parentPath.replace(/[\\/]+$/, "")}${path.slice(1)}` | ||||||||||||||||||||||||||||||||
| : path; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return { isBrowseNavigating, pathInput, setPathInput, navigateToBrowsePath, resolveHomePath }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function useEnvironmentOptions( | ||||||||||||||||||||||||||||||||
|
|
@@ -730,16 +749,76 @@ async function draftWithPrompt( | |||||||||||||||||||||||||||||||
| return draftId; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function useCreateProject(environment: EnvironmentOption | null) { | ||||||||||||||||||||||||||||||||
| function useOpenExistingProject(environment: EnvironmentOption | null) { | ||||||||||||||||||||||||||||||||
| const openNewTaskDraft = useOpenNewTaskDraft(); | ||||||||||||||||||||||||||||||||
| const navigation = useNavigation(); | ||||||||||||||||||||||||||||||||
| const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); | ||||||||||||||||||||||||||||||||
| const updateProject = useAtomCommand(projectEnvironment.update, { | ||||||||||||||||||||||||||||||||
| label: "project unarchive", | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| const projects = useProjects(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return async (workspaceRoot: string, originRoute = getActiveRoute(navigation)) => { | ||||||||||||||||||||||||||||||||
| return async ( | ||||||||||||||||||||||||||||||||
| path: string, | ||||||||||||||||||||||||||||||||
| originRoute: ReturnType<typeof getActiveRoute>, | ||||||||||||||||||||||||||||||||
| clone?: { readonly remoteUrl: string; readonly prompt: string | null }, | ||||||||||||||||||||||||||||||||
| ): Promise<boolean> => { | ||||||||||||||||||||||||||||||||
| if (!environment) return false; | ||||||||||||||||||||||||||||||||
| const existing = findExistingAddProject({ | ||||||||||||||||||||||||||||||||
| projects, | ||||||||||||||||||||||||||||||||
| environmentId: environment.environmentId, | ||||||||||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| if (!existing) return false; | ||||||||||||||||||||||||||||||||
| // A folder holding another repository is not this clone; let the clone report it occupied. | ||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||
| clone && | ||||||||||||||||||||||||||||||||
| existing.repositoryIdentity?.canonicalKey !== | ||||||||||||||||||||||||||||||||
| sourceControlClients.repositoryKeyFromRemote(clone.remoteUrl) | ||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+772
to
+778
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reuse a registered project when its repository identity is unknown. The guard treats a registered project with no Suggested fix
Add a case with Committable suggestion
|
||||||||||||||||||||||||||||||||
| const prompt = clone?.prompt; | ||||||||||||||||||||||||||||||||
| // Adding an archived project's folder again brings the project back. | ||||||||||||||||||||||||||||||||
| const unarchived = | ||||||||||||||||||||||||||||||||
| existing.archivedAt == null || | ||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||
| await updateProject({ | ||||||||||||||||||||||||||||||||
| environmentId: existing.environmentId, | ||||||||||||||||||||||||||||||||
| input: { projectId: existing.id, archived: false }, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| )._tag === "Success"; | ||||||||||||||||||||||||||||||||
| Dialog.alert( | ||||||||||||||||||||||||||||||||
| "Project already exists", | ||||||||||||||||||||||||||||||||
| unarchived ? existing.title : `${existing.title} is still archived. Try again later.`, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| openNewTaskDraft( | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| environmentId: existing.environmentId, | ||||||||||||||||||||||||||||||||
| projectId: existing.id, | ||||||||||||||||||||||||||||||||
| title: existing.title, | ||||||||||||||||||||||||||||||||
| ...(prompt | ||||||||||||||||||||||||||||||||
| ? await draftWithPrompt(existing.environmentId, existing.id, prompt).then( | ||||||||||||||||||||||||||||||||
| (draftId) => ({ draftId }), | ||||||||||||||||||||||||||||||||
| () => ({}), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| : {}), | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| originRoute, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function useCreateProject(environment: EnvironmentOption | null) { | ||||||||||||||||||||||||||||||||
| const openNewTaskDraft = useOpenNewTaskDraft(); | ||||||||||||||||||||||||||||||||
| const navigation = useNavigation(); | ||||||||||||||||||||||||||||||||
| const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); | ||||||||||||||||||||||||||||||||
| const openExistingProject = useOpenExistingProject(environment); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return async ( | ||||||||||||||||||||||||||||||||
| workspaceRoot: string, | ||||||||||||||||||||||||||||||||
| originRoute = getActiveRoute(navigation), | ||||||||||||||||||||||||||||||||
| lookupPath = workspaceRoot, | ||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||
| !environment || | ||||||||||||||||||||||||||||||||
| !canCreateProjectInEnvironment(environment.connectionState) || | ||||||||||||||||||||||||||||||||
|
|
@@ -752,33 +831,7 @@ function useCreateProject(environment: EnvironmentOption | null) { | |||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const existing = findExistingAddProject({ | ||||||||||||||||||||||||||||||||
| projects, | ||||||||||||||||||||||||||||||||
| environmentId: environment.environmentId, | ||||||||||||||||||||||||||||||||
| path: workspaceRoot, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| if (existing) { | ||||||||||||||||||||||||||||||||
| // Adding an archived project's folder again brings the project back. | ||||||||||||||||||||||||||||||||
| const unarchived = | ||||||||||||||||||||||||||||||||
| existing.archivedAt == null || | ||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||
| await updateProject({ | ||||||||||||||||||||||||||||||||
| environmentId: existing.environmentId, | ||||||||||||||||||||||||||||||||
| input: { projectId: existing.id, archived: false }, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| )._tag === "Success"; | ||||||||||||||||||||||||||||||||
| Dialog.alert( | ||||||||||||||||||||||||||||||||
| "Project already exists", | ||||||||||||||||||||||||||||||||
| unarchived ? existing.title : `${existing.title} is still archived. Try again later.`, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| openNewTaskDraft( | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| environmentId: existing.environmentId, | ||||||||||||||||||||||||||||||||
| projectId: existing.id, | ||||||||||||||||||||||||||||||||
| title: existing.title, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| originRoute, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| if (await openExistingProject(lookupPath, originRoute)) { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -1313,8 +1366,9 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st | |||||||||||||||||||||||||||||||
| environment?.environmentId ?? null, | ||||||||||||||||||||||||||||||||
| AuthOrchestrationOperateScope, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| const navigation = useNavigation(); | ||||||||||||||||||||||||||||||||
| const createProject = useCreateProject(environment); | ||||||||||||||||||||||||||||||||
| const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = | ||||||||||||||||||||||||||||||||
| const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput, resolveHomePath } = | ||||||||||||||||||||||||||||||||
| useBrowsePathInput(environment); | ||||||||||||||||||||||||||||||||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||
|
|
@@ -1333,7 +1387,11 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| setIsSubmitting(true); | ||||||||||||||||||||||||||||||||
| const result = await createProject(resolved.path); | ||||||||||||||||||||||||||||||||
| const originRoute = getActiveRoute(navigation); | ||||||||||||||||||||||||||||||||
| const lookupPath = resolved.path.startsWith("~") | ||||||||||||||||||||||||||||||||
| ? await resolveHomePath(resolved.path) | ||||||||||||||||||||||||||||||||
| : resolved.path; | ||||||||||||||||||||||||||||||||
| const result = await createProject(resolved.path, originRoute, lookupPath); | ||||||||||||||||||||||||||||||||
| if (result && AsyncResult.isFailure(result)) { | ||||||||||||||||||||||||||||||||
| setError(errorMessage(Cause.squash(result.cause))); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -1404,11 +1462,9 @@ export function AddProjectDestinationScreen(props: { | |||||||||||||||||||||||||||||||
| ? getCloneDirectoryPath(remoteUrl, true, repositoryName) | ||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||
| const cloneDirectoryName = ghqDirectoryPath?.split("/").at(-1) ?? repositoryName; | ||||||||||||||||||||||||||||||||
| const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = useBrowsePathInput( | ||||||||||||||||||||||||||||||||
| environment, | ||||||||||||||||||||||||||||||||
| cloneDirectoryName, | ||||||||||||||||||||||||||||||||
| ghqDirectoryPath ?? repositoryName, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput, resolveHomePath } = | ||||||||||||||||||||||||||||||||
| useBrowsePathInput(environment, cloneDirectoryName, ghqDirectoryPath ?? repositoryName); | ||||||||||||||||||||||||||||||||
| const openExistingProject = useOpenExistingProject(environment); | ||||||||||||||||||||||||||||||||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -1436,6 +1492,15 @@ export function AddProjectDestinationScreen(props: { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const originRoute = getActiveRoute(navigation); | ||||||||||||||||||||||||||||||||
| setIsSubmitting(true); | ||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||
| await openExistingProject(await resolveHomePath(resolved.path), originRoute, { | ||||||||||||||||||||||||||||||||
| remoteUrl, | ||||||||||||||||||||||||||||||||
| prompt: prompt ?? null, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||
| setIsSubmitting(false); | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const title = inferProjectTitleFromPath(resolved.path); | ||||||||||||||||||||||||||||||||
| const startResult = await startProjectClone({ | ||||||||||||||||||||||||||||||||
| environmentId: environment.environmentId, | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Read the current project list when the existing-project lookup runs.
In the local-folder and clone submit paths, a
~path awaitsresolveHomePathbefore the lookup. The lookup uses theprojectsvalue captured when the submit handler was created. If a project registration arrives during that await, the lookup can miss the destination. The flow then sendsproject.createorstartProjectClonefor a path that already belongs to a project, instead of opening its draft.