| ... | @@ -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"; |
| 13 | import { | 13 | import { |
| | 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"; |
| 43 | import { uuidv4 } from "../../lib/uuid"; | 45 | import { uuidv4 } from "../../lib/uuid"; |
| 44 | import { useAtomCommand } from "../../state/use-atom-command"; | 46 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 45 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; | 47 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; |
| 46 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; | 48 | import { |
| | 49 | useRemoteEnvironmentRuntime, |
| | 50 | useSavedRemoteConnections, |
| | 51 | } from "../../state/use-remote-environment-registry"; |
| 47 | | 52 | |
| 48 | interface EnvironmentOption { | 53 | interface 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 | } |
| 223 | | 228 | |
| | 229 | function 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 | |
| 224 | function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> { | 286 | function 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 > 0 | 713 | 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: { |
| 662 | export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) { | 725 | export 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); |
| 670 | | 732 | |
| 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]); |
| 696 | | 753 | |
| 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 | <PrimaryActionButton | 764 | <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 | <FolderBrowser | 770 | <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); |
| 743 | | 800 | |
| 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 | ]); |
| 780 | | 840 | |
| 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 | <PrimaryActionButton | 859 | <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 | <FolderBrowser | 865 | <FolderBrowser |
| 806 | environment={environment} | 866 | environment={environment} |
| | 867 | navigateToBrowsePath={navigateToBrowsePath} |
| 807 | pathInput={pathInput} | 868 | pathInput={pathInput} |
| 808 | setPathInput={setPathInput} | 869 | setPathInput={setPathInput} |
| 809 | /> | 870 | /> |