import { Reducer, useCallback, useEffect, useMemo, useReducer } from 'react'; import { useSelector } from 'react-redux'; import { isRemoteSpace } from '../../models/space'; import { Project } from '../../sync/types'; import { ProjectWithTeam } from '../../sync/vcs/normalize-project-team'; import { pullProject } from '../../sync/vcs/pull-project'; import { VCS } from '../../sync/vcs/vcs'; import { showAlert } from '../components/modals'; import { selectActiveSpace, selectAllWorkspaces, selectIsLoggedIn, selectRemoteSpaces } from '../redux/selectors'; import { useSafeReducerDispatch } from './use-safe-reducer-dispatch'; interface State { loading: boolean; localProjects: Project[]; remoteProjects: ProjectWithTeam[]; pullingProjects: Record; } const initialState: State = { loading: false, localProjects: [], remoteProjects: [], pullingProjects: {}, }; type Action = | { type: 'loadProjects' } | { type: 'saveProjects', local: State['localProjects'], remote: State['remoteProjects']} | { type: 'startPullingProject', projectId: string } | { type: 'stopPullingProject', projectId: string } const reducer: Reducer = (prevState, action) => { switch (action.type) { case 'loadProjects': return { ...prevState, loading: true }; case 'saveProjects': return { ...prevState, localProjects: action.local, remoteProjects: action.remote, loading: false }; case 'startPullingProject': return { ...prevState, pullingProjects: { ...prevState.pullingProjects, [action.projectId]: true } }; case 'stopPullingProject': return { ...prevState, pullingProjects: { ...prevState.pullingProjects, [action.projectId]: false } }; default: return prevState; } }; export const useRemoteWorkspaces = (vcs?: VCS) => { // Fetch from redux const workspaces = useSelector(selectAllWorkspaces); const activeSpace = useSelector(selectActiveSpace); const remoteSpaces = useSelector(selectRemoteSpaces); const isLoggedIn = useSelector(selectIsLoggedIn); // Local state const [{ loading, localProjects, remoteProjects, pullingProjects }, _dispatch] = useReducer(reducer, initialState); const dispatch = useSafeReducerDispatch(_dispatch); // Refresh remote spaces const refresh = useCallback(async () => { if (!vcs || !isLoggedIn || !isRemoteSpace(activeSpace)) { return; } dispatch({ type: 'loadProjects' }); const remote = await vcs.remoteProjects(activeSpace.remoteId); const local = await vcs.localProjects(); dispatch({ type: 'saveProjects', local, remote }); }, [vcs, isLoggedIn, activeSpace, dispatch]); // Find remote spaces that haven't been pulled const missingProjects = useMemo(() => remoteProjects.filter(({ id, rootDocumentId }) => { const localProjectExists = localProjects.find(p => p.id === id); const workspaceExists = workspaces.find(w => w._id === rootDocumentId); // Mark as missing if: // - the project doesn't yet exists locally // - the project exists locally but somehow the workspace doesn't anymore return !(workspaceExists && localProjectExists); }), [localProjects, remoteProjects, workspaces]); // Pull a remote space const pull = useCallback(async (project: ProjectWithTeam) => { if (!vcs) { throw new Error('VCS is not defined'); } dispatch({ type: 'startPullingProject', projectId: project.id }); try { // Clone old VCS so we don't mess anything up while working on other projects const newVCS = vcs.newInstance(); // Remove all projects for workspace first await newVCS.removeProjectsForRoot(project.rootDocumentId); await pullProject({ vcs: newVCS, project, remoteSpaces }); await refresh(); } catch (err) { showAlert({ title: 'Pull Error', message: `Failed to pull workspace. ${err.message}`, }); } finally { dispatch({ type: 'stopPullingProject', projectId: project.id }); } }, [vcs, refresh, remoteSpaces, dispatch]); // If the refresh callback changes, refresh useEffect(() => { (async () => { await refresh(); })(); }, [refresh]); return { loading, missingProjects, pullingProjects, refresh, pull, }; };