| ... | ... | @@ -11,6 +11,8 @@ import { |
| 11 | 11 | type AddProjectRemoteSource, |
| 12 | 12 | } from "@t3tools/client-runtime/operations/projects"; |
| 13 | 13 | import { |
| 14 | canPreloadBrowsePath, |
| 15 | createBrowseNavigationCoordinator, |
| 14 | 16 | filterFilesystemBrowseEntries, |
| 15 | 17 | getFilesystemBrowsePath, |
| 16 | 18 | } from "@t3tools/client-runtime/state/filesystem"; |
| ... | ... | @@ -43,7 +45,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; |
| 43 | 45 | import { uuidv4 } from "../../lib/uuid"; |
| 44 | 46 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 45 | 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 | 53 | interface EnvironmentOption { |
| 49 | 54 | readonly environmentId: EnvironmentId; |
| ... | ... | @@ -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 | 286 | function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> { |
| 225 | 287 | const serverConfigByEnvironmentId = useServerConfigs(); |
| 226 | 288 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| ... | ... | @@ -585,6 +647,7 @@ function FolderBrowser(props: { |
| 585 | 647 | readonly environment: EnvironmentOption; |
| 586 | 648 | readonly pathInput: string; |
| 587 | 649 | readonly setPathInput: (path: string) => void; |
| 650 | readonly navigateToBrowsePath: (path: string) => Promise<boolean>; |
| 588 | 651 | }) { |
| 589 | 652 | const accentColor = useThemeColor("--color-icon-muted"); |
| 590 | 653 | const browsePath = useMemo( |
| ... | ... | @@ -633,7 +696,7 @@ function FolderBrowser(props: { |
| 633 | 696 | right={null} |
| 634 | 697 | onPress={() => { |
| 635 | 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 | 713 | browsePath.directoryPath.length > 0 |
| 651 | 714 | ? appendBrowsePathSegment(browsePath.directoryPath, entry.name) |
| 652 | 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 | 725 | export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) { |
| 663 | 726 | const environment = useEnvironmentFromParam(props.environmentId); |
| 664 | 727 | const createProject = useCreateProject(environment); |
| 665 | | const [pathInput, setPathInput] = useState(() => |
| 666 | | getAddProjectInitialQuery(environment?.baseDirectory), |
| 667 | | ); |
| 728 | const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = |
| 729 | useBrowsePathInput(environment); |
| 668 | 730 | const [isSubmitting, setIsSubmitting] = useState(false); |
| 669 | 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 | 733 | const submitPath = useCallback(async () => { |
| 677 | | if (!environment || isSubmitting) return; |
| 734 | if (!environment || isBrowseNavigating || isSubmitting) return; |
| 678 | 735 | setError(null); |
| 679 | 736 | const resolved = resolveAddProjectPath({ |
| 680 | 737 | rawPath: pathInput, |
| ... | ... | @@ -692,7 +749,7 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st |
| 692 | 749 | setError(errorMessage(Cause.squash(result.cause))); |
| 693 | 750 | } |
| 694 | 751 | setIsSubmitting(false); |
| 695 | | }, [createProject, environment, isSubmitting, pathInput]); |
| 752 | }, [createProject, environment, isBrowseNavigating, isSubmitting, pathInput]); |
| 696 | 753 | |
| 697 | 754 | return ( |
| 698 | 755 | <AddProjectShell> |
| ... | ... | @@ -706,12 +763,13 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st |
| 706 | 763 | /> |
| 707 | 764 | <PrimaryActionButton |
| 708 | 765 | label="Add project" |
| 709 | | disabled={isSubmitting} |
| 766 | disabled={isBrowseNavigating || isSubmitting} |
| 710 | 767 | onPress={() => void submitPath()} |
| 711 | 768 | loading={isSubmitting} |
| 712 | 769 | /> |
| 713 | 770 | <FolderBrowser |
| 714 | 771 | environment={environment} |
| 772 | navigateToBrowsePath={navigateToBrowsePath} |
| 715 | 773 | pathInput={pathInput} |
| 716 | 774 | setPathInput={setPathInput} |
| 717 | 775 | /> |
| ... | ... | @@ -735,19 +793,13 @@ export function AddProjectDestinationScreen(props: { |
| 735 | 793 | const createProject = useCreateProject(environment); |
| 736 | 794 | const remoteUrl = stringParam(props.remoteUrl); |
| 737 | 795 | const repositoryTitle = stringParam(props.repositoryTitle); |
| 738 | | const [pathInput, setPathInput] = useState(() => |
| 739 | | getAddProjectInitialQuery(environment?.baseDirectory), |
| 740 | | ); |
| 796 | const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = |
| 797 | useBrowsePathInput(environment); |
| 741 | 798 | const [isSubmitting, setIsSubmitting] = useState(false); |
| 742 | 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 | 801 | const submitPath = useCallback(async () => { |
| 750 | | if (!environment || !remoteUrl || isSubmitting) return; |
| 802 | if (!environment || !remoteUrl || isBrowseNavigating || isSubmitting) return; |
| 751 | 803 | setError(null); |
| 752 | 804 | const resolved = resolveAddProjectPath({ |
| 753 | 805 | rawPath: pathInput, |
| ... | ... | @@ -776,7 +828,15 @@ export function AddProjectDestinationScreen(props: { |
| 776 | 828 | } |
| 777 | 829 | } |
| 778 | 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 | 841 | return ( |
| 782 | 842 | <AddProjectShell> |
| ... | ... | @@ -798,12 +858,13 @@ export function AddProjectDestinationScreen(props: { |
| 798 | 858 | /> |
| 799 | 859 | <PrimaryActionButton |
| 800 | 860 | label="Clone project" |
| 801 | | disabled={isSubmitting || !remoteUrl} |
| 861 | disabled={isBrowseNavigating || isSubmitting || !remoteUrl} |
| 802 | 862 | onPress={() => void submitPath()} |
| 803 | 863 | loading={isSubmitting} |
| 804 | 864 | /> |
| 805 | 865 | <FolderBrowser |
| 806 | 866 | environment={environment} |
| 867 | navigateToBrowsePath={navigateToBrowsePath} |
| 807 | 868 | pathInput={pathInput} |
| 808 | 869 | setPathInput={setPathInput} |
| 809 | 870 | /> |