authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-29 01:46:59+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 01:46:59+02:00
log8fe8f9a9c1486528f689a7499c2eb78217d35caf
tree41d27cf3381a59bfcac5e9386e4bdc7593b87af7
parent936593c2583c6640a07fc310f97bd5d0c27a7c31
signature Signed by PGP key B5690EEEBB952194

fix(mobile): defer filesystem navigation (#4799)

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 86 insertions(+), 25 deletions(-)

apps/mobile/src/features/projects/AddProjectScreen.tsx+86-25
...@@ -11,6 +11,8 @@ import {...@@ -11,6 +11,8 @@ import {
11 type AddProjectRemoteSource,11 type AddProjectRemoteSource,
12} from "@t3tools/client-runtime/operations/projects";12} from "@t3tools/client-runtime/operations/projects";
13import {13import {
14 canPreloadBrowsePath,
15 createBrowseNavigationCoordinator,
14 filterFilesystemBrowseEntries,16 filterFilesystemBrowseEntries,
15 getFilesystemBrowsePath,17 getFilesystemBrowsePath,
16} from "@t3tools/client-runtime/state/filesystem";18} from "@t3tools/client-runtime/state/filesystem";
...@@ -43,7 +45,10 @@ import { useThemeColor } from "../../lib/useThemeColor";...@@ -43,7 +45,10 @@ import { useThemeColor } from "../../lib/useThemeColor";
43import { uuidv4 } from "../../lib/uuid";45import { uuidv4 } from "../../lib/uuid";
44import { useAtomCommand } from "../../state/use-atom-command";46import { useAtomCommand } from "../../state/use-atom-command";
45import { useAtomQueryRunner } from "../../state/use-atom-query-runner";47import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
46import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";48import {
49 useRemoteEnvironmentRuntime,
50 useSavedRemoteConnections,
51} from "../../state/use-remote-environment-registry";
4752
48interface EnvironmentOption {53interface EnvironmentOption {
49 readonly environmentId: EnvironmentId;54 readonly environmentId: EnvironmentId;
...@@ -221,6 +226,63 @@ function ProjectPathInput(props: {...@@ -221,6 +226,63 @@ function ProjectPathInput(props: {
221 );226 );
222}227}
223228
229function useBrowsePathInput(environment: EnvironmentOption | null) {
230 const [pathInput, commitPathInput] = useState(() =>
231 getAddProjectInitialQuery(environment?.baseDirectory),
232 );
233 const environmentRuntime = useRemoteEnvironmentRuntime(environment?.environmentId ?? null);
234 const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, {
235 reportFailure: false,
236 reportDefect: false,
237 });
238 const [browseNavigation] = useState(createBrowseNavigationCoordinator);
239 const [isBrowseNavigating, setIsBrowseNavigating] = useState(false);
240 const setPathInput = useCallback(
241 (path: string) => {
242 browseNavigation.invalidate();
243 setIsBrowseNavigating(false);
244 commitPathInput(path);
245 },
246 [browseNavigation],
247 );
248 const navigateToBrowsePath = useCallback(
249 async (path: string) => {
250 setIsBrowseNavigating(true);
251 const committed = await browseNavigation.run(
252 async () => {
253 if (environment && canPreloadBrowsePath(environmentRuntime?.connectionState)) {
254 await loadBrowsePath({
255 environmentId: environment.environmentId,
256 input: { partialPath: path },
257 });
258 }
259 },
260 () => commitPathInput(path),
261 );
262 if (committed) {
263 setIsBrowseNavigating(false);
264 }
265 return committed;
266 },
267 [browseNavigation, environment, environmentRuntime?.connectionState, loadBrowsePath],
268 );
269
270 useEffect(() => {
271 if (environment) {
272 setPathInput(getAddProjectInitialQuery(environment.baseDirectory));
273 }
274 }, [environment, setPathInput]);
275
276 useEffect(
277 () => () => {
278 browseNavigation.invalidate();
279 },
280 [browseNavigation],
281 );
282
283 return { isBrowseNavigating, pathInput, setPathInput, navigateToBrowsePath };
284}
285
224function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> {286function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> {
225 const serverConfigByEnvironmentId = useServerConfigs();287 const serverConfigByEnvironmentId = useServerConfigs();
226 const { savedConnectionsById } = useSavedRemoteConnections();288 const { savedConnectionsById } = useSavedRemoteConnections();
...@@ -585,6 +647,7 @@ function FolderBrowser(props: {...@@ -585,6 +647,7 @@ function FolderBrowser(props: {
585 readonly environment: EnvironmentOption;647 readonly environment: EnvironmentOption;
586 readonly pathInput: string;648 readonly pathInput: string;
587 readonly setPathInput: (path: string) => void;649 readonly setPathInput: (path: string) => void;
650 readonly navigateToBrowsePath: (path: string) => Promise<boolean>;
588}) {651}) {
589 const accentColor = useThemeColor("--color-icon-muted");652 const accentColor = useThemeColor("--color-icon-muted");
590 const browsePath = useMemo(653 const browsePath = useMemo(
...@@ -633,7 +696,7 @@ function FolderBrowser(props: {...@@ -633,7 +696,7 @@ function FolderBrowser(props: {
633 right={null}696 right={null}
634 onPress={() => {697 onPress={() => {
635 if (browsePath.parentPath) {698 if (browsePath.parentPath) {
636 props.setPathInput(browsePath.parentPath);699 void props.navigateToBrowsePath(browsePath.parentPath);
637 }700 }
638 }}701 }}
639 />702 />
...@@ -650,7 +713,7 @@ function FolderBrowser(props: {...@@ -650,7 +713,7 @@ function FolderBrowser(props: {
650 browsePath.directoryPath.length > 0713 browsePath.directoryPath.length > 0
651 ? appendBrowsePathSegment(browsePath.directoryPath, entry.name)714 ? appendBrowsePathSegment(browsePath.directoryPath, entry.name)
652 : ensureBrowseDirectoryPath(entry.fullPath);715 : ensureBrowseDirectoryPath(entry.fullPath);
653 props.setPathInput(nextPath);716 void props.navigateToBrowsePath(nextPath);
654 }}717 }}
655 />718 />
656 ))}719 ))}
...@@ -662,19 +725,13 @@ function FolderBrowser(props: {...@@ -662,19 +725,13 @@ function FolderBrowser(props: {
662export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) {725export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) {
663 const environment = useEnvironmentFromParam(props.environmentId);726 const environment = useEnvironmentFromParam(props.environmentId);
664 const createProject = useCreateProject(environment);727 const createProject = useCreateProject(environment);
665 const [pathInput, setPathInput] = useState(() =>728 const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } =
666 getAddProjectInitialQuery(environment?.baseDirectory),729 useBrowsePathInput(environment);
667 );
668 const [isSubmitting, setIsSubmitting] = useState(false);730 const [isSubmitting, setIsSubmitting] = useState(false);
669 const [error, setError] = useState<string | null>(null);731 const [error, setError] = useState<string | null>(null);
670732
671 useEffect(() => {
672 if (!environment) return;
673 setPathInput(getAddProjectInitialQuery(environment.baseDirectory));
674 }, [environment]);
675
676 const submitPath = useCallback(async () => {733 const submitPath = useCallback(async () => {
677 if (!environment || isSubmitting) return;734 if (!environment || isBrowseNavigating || isSubmitting) return;
678 setError(null);735 setError(null);
679 const resolved = resolveAddProjectPath({736 const resolved = resolveAddProjectPath({
680 rawPath: pathInput,737 rawPath: pathInput,
...@@ -692,7 +749,7 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st...@@ -692,7 +749,7 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st
692 setError(errorMessage(Cause.squash(result.cause)));749 setError(errorMessage(Cause.squash(result.cause)));
693 }750 }
694 setIsSubmitting(false);751 setIsSubmitting(false);
695 }, [createProject, environment, isSubmitting, pathInput]);752 }, [createProject, environment, isBrowseNavigating, isSubmitting, pathInput]);
696753
697 return (754 return (
698 <AddProjectShell>755 <AddProjectShell>
...@@ -706,12 +763,13 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st...@@ -706,12 +763,13 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st
706 />763 />
707 <PrimaryActionButton764 <PrimaryActionButton
708 label="Add project"765 label="Add project"
709 disabled={isSubmitting}766 disabled={isBrowseNavigating || isSubmitting}
710 onPress={() => void submitPath()}767 onPress={() => void submitPath()}
711 loading={isSubmitting}768 loading={isSubmitting}
712 />769 />
713 <FolderBrowser770 <FolderBrowser
714 environment={environment}771 environment={environment}
772 navigateToBrowsePath={navigateToBrowsePath}
715 pathInput={pathInput}773 pathInput={pathInput}
716 setPathInput={setPathInput}774 setPathInput={setPathInput}
717 />775 />
...@@ -735,19 +793,13 @@ export function AddProjectDestinationScreen(props: {...@@ -735,19 +793,13 @@ export function AddProjectDestinationScreen(props: {
735 const createProject = useCreateProject(environment);793 const createProject = useCreateProject(environment);
736 const remoteUrl = stringParam(props.remoteUrl);794 const remoteUrl = stringParam(props.remoteUrl);
737 const repositoryTitle = stringParam(props.repositoryTitle);795 const repositoryTitle = stringParam(props.repositoryTitle);
738 const [pathInput, setPathInput] = useState(() =>796 const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } =
739 getAddProjectInitialQuery(environment?.baseDirectory),797 useBrowsePathInput(environment);
740 );
741 const [isSubmitting, setIsSubmitting] = useState(false);798 const [isSubmitting, setIsSubmitting] = useState(false);
742 const [error, setError] = useState<string | null>(null);799 const [error, setError] = useState<string | null>(null);
743800
744 useEffect(() => {
745 if (!environment) return;
746 setPathInput(getAddProjectInitialQuery(environment.baseDirectory));
747 }, [environment]);
748
749 const submitPath = useCallback(async () => {801 const submitPath = useCallback(async () => {
750 if (!environment || !remoteUrl || isSubmitting) return;802 if (!environment || !remoteUrl || isBrowseNavigating || isSubmitting) return;
751 setError(null);803 setError(null);
752 const resolved = resolveAddProjectPath({804 const resolved = resolveAddProjectPath({
753 rawPath: pathInput,805 rawPath: pathInput,
...@@ -776,7 +828,15 @@ export function AddProjectDestinationScreen(props: {...@@ -776,7 +828,15 @@ export function AddProjectDestinationScreen(props: {
776 }828 }
777 }829 }
778 setIsSubmitting(false);830 setIsSubmitting(false);
779 }, [cloneRepository, createProject, environment, isSubmitting, pathInput, remoteUrl]);831 }, [
832 cloneRepository,
833 createProject,
834 environment,
835 isBrowseNavigating,
836 isSubmitting,
837 pathInput,
838 remoteUrl,
839 ]);
780840
781 return (841 return (
782 <AddProjectShell>842 <AddProjectShell>
...@@ -798,12 +858,13 @@ export function AddProjectDestinationScreen(props: {...@@ -798,12 +858,13 @@ export function AddProjectDestinationScreen(props: {
798 />858 />
799 <PrimaryActionButton859 <PrimaryActionButton
800 label="Clone project"860 label="Clone project"
801 disabled={isSubmitting || !remoteUrl}861 disabled={isBrowseNavigating || isSubmitting || !remoteUrl}
802 onPress={() => void submitPath()}862 onPress={() => void submitPath()}
803 loading={isSubmitting}863 loading={isSubmitting}
804 />864 />
805 <FolderBrowser865 <FolderBrowser
806 environment={environment}866 environment={environment}
867 navigateToBrowsePath={navigateToBrowsePath}
807 pathInput={pathInput}868 pathInput={pathInput}
808 setPathInput={setPathInput}869 setPathInput={setPathInput}
809 />870 />