Skip to content

Commit ec141c1

Browse files
inayayousfijuliusmarmingecodex
authored
fix(clients): default clone destination to folder plus repo name (#5989)
Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 2fc6762 commit ec141c1

8 files changed

Lines changed: 340 additions & 30 deletions

File tree

apps/mobile/src/features/projects/AddProjectDestinationRoute.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type AddProjectDestinationRouteParams = {
66
readonly source?: string | string[];
77
readonly remoteUrl?: string | string[];
88
readonly repositoryTitle?: string | string[];
9+
readonly repositoryName?: string | string[];
910
};
1011

1112
export function AddProjectDestinationRoute({

apps/mobile/src/features/projects/AddProjectScreen.tsx

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
canCreateProjectInEnvironment,
88
findExistingAddProject,
99
getAddProjectInitialQuery,
10+
getCloneDestinationBrowsePath,
11+
getCloneDestinationPath,
12+
getCloneDirectoryName,
1013
resolveAddProjectPath,
1114
sortAddProjectProviderSources,
1215
type AddProjectRemoteSource,
@@ -23,8 +26,8 @@ import {
2326
} from "@t3tools/client-runtime/state/filesystem";
2427
import {
2528
appendBrowsePathSegment,
26-
ensureBrowseDirectoryPath,
2729
inferProjectTitleFromPath,
30+
isWindowsPlatform,
2831
} from "@t3tools/client-runtime/state/projects";
2932
import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
3033
import { CommonActions, StackActions, useNavigation } from "@react-navigation/native";
@@ -236,11 +239,18 @@ function ProjectPathInput(props: {
236239
);
237240
}
238241

239-
function useBrowsePathInput(environment: EnvironmentOption | null) {
242+
// `pinnedDirectoryName` is the repository folder the clone destination keeps
243+
// appended to whatever folder the user browses to. The plain add-project flow
244+
// passes nothing, so it keeps proposing the browsed folder itself.
245+
function useBrowsePathInput(environment: EnvironmentOption | null, pinnedDirectoryName = "") {
240246
const environmentId = environment?.environmentId ?? null;
241247
const environmentBaseDirectory = environment?.baseDirectory ?? null;
248+
const clonePathCaseSensitive = !isWindowsPlatform(environment?.platform ?? "");
242249
const [pathInput, commitPathInput] = useState(() =>
243-
getAddProjectInitialQuery(environmentBaseDirectory),
250+
getCloneDestinationPath(
251+
getAddProjectInitialQuery(environmentBaseDirectory),
252+
pinnedDirectoryName,
253+
),
244254
);
245255
const previousEnvironmentIdRef = useRef(environmentId);
246256
const environmentRuntime = useRemoteEnvironmentRuntime(environmentId);
@@ -259,33 +269,60 @@ function useBrowsePathInput(environment: EnvironmentOption | null) {
259269
[browseNavigation],
260270
);
261271
const navigateToBrowsePath = useCallback(
262-
async (path: string) => {
272+
async (input: {
273+
readonly browseDirectoryPath: string;
274+
readonly selectedDirectoryName?: string;
275+
}) => {
276+
const selectedDirectoryPath = input.selectedDirectoryName
277+
? appendBrowsePathSegment(input.browseDirectoryPath, input.selectedDirectoryName)
278+
: input.browseDirectoryPath;
279+
const nextPathInput =
280+
pinnedDirectoryName && input.selectedDirectoryName
281+
? getCloneDestinationBrowsePath({
282+
browseDirectoryPath: input.browseDirectoryPath,
283+
selectedDirectoryName: input.selectedDirectoryName,
284+
cloneDirectoryName: pinnedDirectoryName,
285+
caseSensitive: clonePathCaseSensitive,
286+
})
287+
: getCloneDestinationPath(selectedDirectoryPath, pinnedDirectoryName);
263288
setIsBrowseNavigating(true);
264289
const committed = await browseNavigation.run(
265290
async () => {
266291
if (environment && canPreloadBrowsePath(environmentRuntime?.connectionState)) {
267292
await loadBrowsePath({
268293
environmentId: environment.environmentId,
269-
input: { partialPath: path },
294+
input: { partialPath: selectedDirectoryPath },
270295
});
271296
}
272297
},
273-
() => commitPathInput(path),
298+
() => commitPathInput(nextPathInput),
274299
);
275300
if (committed) {
276301
setIsBrowseNavigating(false);
277302
}
278303
return committed;
279304
},
280-
[browseNavigation, environment, environmentRuntime?.connectionState, loadBrowsePath],
305+
[
306+
browseNavigation,
307+
clonePathCaseSensitive,
308+
environment,
309+
environmentRuntime?.connectionState,
310+
loadBrowsePath,
311+
pinnedDirectoryName,
312+
],
281313
);
282314

283315
useEffect(() => {
284316
if (environmentId !== null && environmentId !== previousEnvironmentIdRef.current) {
285317
previousEnvironmentIdRef.current = environmentId;
286-
setPathInput(getAddProjectInitialQuery(environmentBaseDirectory));
318+
setPathInput(
319+
getCloneDestinationPath(
320+
getAddProjectInitialQuery(environmentBaseDirectory),
321+
pinnedDirectoryName,
322+
),
323+
);
287324
}
288-
}, [environmentBaseDirectory, environmentId, setPathInput]);
325+
}, [environmentBaseDirectory, environmentId, pinnedDirectoryName, setPathInput]);
289326

290327
useEffect(
291328
() => () => {
@@ -632,6 +669,7 @@ export function AddProjectRepositoryScreen(props: {
632669
source,
633670
remoteUrl,
634671
repositoryTitle: remoteUrl,
672+
repositoryName: getCloneDirectoryName(remoteUrl),
635673
}),
636674
);
637675
setIsSubmitting(false);
@@ -655,6 +693,7 @@ export function AddProjectRepositoryScreen(props: {
655693
source,
656694
remoteUrl: repository.sshUrl,
657695
repositoryTitle: repository.nameWithOwner,
696+
repositoryName: getCloneDirectoryName(repository.nameWithOwner),
658697
}),
659698
);
660699
}
@@ -698,7 +737,11 @@ function FolderBrowser(props: {
698737
readonly environment: EnvironmentOption;
699738
readonly pathInput: string;
700739
readonly setPathInput: (path: string) => void;
701-
readonly navigateToBrowsePath: (path: string) => Promise<boolean>;
740+
readonly navigateToBrowsePath: (input: {
741+
readonly browseDirectoryPath: string;
742+
readonly selectedDirectoryName?: string;
743+
}) => Promise<boolean>;
744+
readonly pinnedDirectoryName?: string;
702745
}) {
703746
const accentColor = useThemeColor("--color-icon-muted");
704747
const browsePath = useMemo(
@@ -717,9 +760,16 @@ function FolderBrowser(props: {
717760
input: browseInput,
718761
}),
719762
);
763+
// A pinned repository folder does not exist yet, so filtering the listing by
764+
// it would empty the folder picker. Anything the user typed still filters.
765+
const pinnedDirectoryName = props.pinnedDirectoryName ?? "";
766+
const pinnedDirectoryMatches = isWindowsPlatform(props.environment.platform)
767+
? browsePath.filterQuery.toLowerCase() === pinnedDirectoryName.toLowerCase()
768+
: browsePath.filterQuery === pinnedDirectoryName;
769+
const browseFilterQuery = pinnedDirectoryMatches ? "" : browsePath.filterQuery;
720770
const { visibleEntries: visibleBrowseEntries } = useMemo(
721-
() => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browsePath.filterQuery),
722-
[browsePath.filterQuery, browseState.data?.entries],
771+
() => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browseFilterQuery),
772+
[browseFilterQuery, browseState.data?.entries],
723773
);
724774

725775
return (
@@ -747,7 +797,9 @@ function FolderBrowser(props: {
747797
right={null}
748798
onPress={() => {
749799
if (browsePath.parentPath) {
750-
void props.navigateToBrowsePath(browsePath.parentPath);
800+
void props.navigateToBrowsePath({
801+
browseDirectoryPath: browsePath.parentPath,
802+
});
751803
}
752804
}}
753805
/>
@@ -760,11 +812,10 @@ function FolderBrowser(props: {
760812
isFirst={index === 0 && !browsePath.canBrowseUp}
761813
right={null}
762814
onPress={() => {
763-
const nextPath =
764-
browsePath.directoryPath.length > 0
765-
? appendBrowsePathSegment(browsePath.directoryPath, entry.name)
766-
: ensureBrowseDirectoryPath(entry.fullPath);
767-
void props.navigateToBrowsePath(nextPath);
815+
void props.navigateToBrowsePath({
816+
browseDirectoryPath: browsePath.directoryPath,
817+
selectedDirectoryName: entry.name,
818+
});
768819
}}
769820
/>
770821
))}
@@ -836,6 +887,7 @@ export function AddProjectDestinationScreen(props: {
836887
readonly environmentId?: string | string[];
837888
readonly remoteUrl?: string | string[];
838889
readonly repositoryTitle?: string | string[];
890+
readonly repositoryName?: string | string[];
839891
}) {
840892
const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, {
841893
reportFailure: false,
@@ -844,8 +896,15 @@ export function AddProjectDestinationScreen(props: {
844896
const createProject = useCreateProject(environment);
845897
const remoteUrl = stringParam(props.remoteUrl);
846898
const repositoryTitle = stringParam(props.repositoryTitle);
847-
const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } =
848-
useBrowsePathInput(environment);
899+
// A lookup derives this from "owner/repo", a pasted clone URL from its own
900+
// last segment. Older links without the param keep the browsed folder.
901+
// Trim once here: the path input and the folder-list filter must compare the
902+
// same value, or a deep link with a padded param empties the folder picker.
903+
const repositoryName = stringParam(props.repositoryName)?.trim() ?? "";
904+
const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = useBrowsePathInput(
905+
environment,
906+
repositoryName,
907+
);
849908
const [isSubmitting, setIsSubmitting] = useState(false);
850909
const [error, setError] = useState<string | null>(null);
851910

@@ -918,6 +977,7 @@ export function AddProjectDestinationScreen(props: {
918977
navigateToBrowsePath={navigateToBrowsePath}
919978
pathInput={pathInput}
920979
setPathInput={setPathInput}
980+
pinnedDirectoryName={repositoryName}
921981
/>
922982
</>
923983
) : (

apps/web/src/components/CommandPalette.logic.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
buildBrowseGroups,
77
buildThreadActionItems,
88
enumerateCommandPaletteItems,
9+
filterPinnedBrowseEntries,
910
filterCommandPaletteGroups,
1011
reduceCommandPaletteUiState,
1112
type CommandPaletteGroup,
@@ -371,3 +372,39 @@ describe("buildBrowseGroups", () => {
371372
expect(actionSettled).toBe(true);
372373
});
373374
});
375+
376+
describe("filterPinnedBrowseEntries", () => {
377+
const entries = [
378+
{ name: "repo", fullPath: "/projects/repo" },
379+
{ name: "work", fullPath: "/projects/work" },
380+
];
381+
382+
it("shows sibling folders without losing an existing pinned destination", () => {
383+
expect(
384+
filterPinnedBrowseEntries({
385+
browseEntries: entries,
386+
filterQuery: "repo",
387+
pinnedDirectoryName: "repo",
388+
caseSensitive: true,
389+
}),
390+
).toEqual({ visibleEntries: entries, exactEntry: entries[0] });
391+
});
392+
393+
it("matches an existing pinned destination without Windows casing", () => {
394+
const windowsEntries = [
395+
{ name: "Repo", fullPath: "C:\\projects\\Repo" },
396+
{ name: "work", fullPath: "C:\\projects\\work" },
397+
];
398+
expect(
399+
filterPinnedBrowseEntries({
400+
browseEntries: windowsEntries,
401+
filterQuery: "repo",
402+
pinnedDirectoryName: "repo",
403+
caseSensitive: false,
404+
}),
405+
).toEqual({
406+
visibleEntries: windowsEntries,
407+
exactEntry: windowsEntries[0],
408+
});
409+
});
410+
});

apps/web/src/components/CommandPalette.logic.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type KeybindingCommand,
44
THREAD_JUMP_KEYBINDING_COMMANDS,
55
} from "@t3tools/contracts";
6+
import { filterFilesystemBrowseEntries } from "@t3tools/client-runtime/state/filesystem";
67
import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
78
import * as Arr from "effect/Array";
89
import * as Result from "effect/Result";
@@ -397,6 +398,25 @@ export function buildBrowseGroups(input: {
397398
return [{ value: "directories", label: "Directories", items }];
398399
}
399400

401+
export function filterPinnedBrowseEntries(input: {
402+
browseEntries: ReadonlyArray<FilesystemBrowseEntry>;
403+
filterQuery: string;
404+
pinnedDirectoryName: string;
405+
caseSensitive: boolean;
406+
}): ReturnType<typeof filterFilesystemBrowseEntries> {
407+
const namesMatch = (left: string, right: string) =>
408+
input.caseSensitive ? left === right : left.toLowerCase() === right.toLowerCase();
409+
const visibleFilterQuery = namesMatch(input.filterQuery, input.pinnedDirectoryName)
410+
? ""
411+
: input.filterQuery;
412+
const { visibleEntries } = filterFilesystemBrowseEntries(input.browseEntries, visibleFilterQuery);
413+
const exactEntry =
414+
input.filterQuery.length > 0
415+
? (input.browseEntries.find((entry) => namesMatch(entry.name, input.filterQuery)) ?? null)
416+
: null;
417+
return { visibleEntries, exactEntry };
418+
}
419+
400420
export function getCommandPaletteMode(input: {
401421
currentView: CommandPaletteView | null;
402422
isBrowsing: boolean;

0 commit comments

Comments
 (0)