import { styled } from '@mui/material/styles'; import useDebouncedCallback from 'beautiful-react-hooks/useDebouncedCallback'; import Promise from 'bluebird'; import { ClientContext, GraphQLClient, useMutation, useQuery } from 'graphql-hooks'; import { trim } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import CachedIcon from '@mui/icons-material/Cached'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; import FolderIcon from '@mui/icons-material/Folder'; import { Button, LinearProgress, List, ListItemButton, ListItemIcon, ListItemText, TextField } from '@mui/material'; import { GITHUB_GRAPHQL_API } from '@/constants/auth'; import { useUserInfoObservable } from '@services/auth/hooks'; import { IGithubSearchNode, IGithubSearchRepoQuery } from './interfaces'; const RepoSearchContainer = styled('div')` margin-top: 20px; `; const RepoSearchInput = styled(TextField)``; const ReloadButton = styled(Button)` white-space: nowrap; width: 100%; `; const SEARCH_REPO_QUERY = ` query SearchRepo($queryString: String!, $login: String!) { repositoryOwner(login: $login) { id } search(query: $queryString, type: REPOSITORY, first: 10) { repositoryCount edges { node { ... on Repository { name url } } } } } `; const CREATE_REPO_MUTATION = ` mutation CreateRepository($repoName: String!, $homepageUrl: URI, $ownerId: ID!, $visibility: RepositoryVisibility!) { createRepository (input: { name: $repoName, description: "A non-linear Wiki created using TidGi-Desktop.", hasIssuesEnabled: true, hasWikiEnabled: false, homepageUrl: $homepageUrl, ownerId: $ownerId, visibility: $visibility }) { repository { name homepageUrl } } } `; interface Props { githubWikiUrl: string; githubWikiUrlSetter: (value: string) => void; isCreateMainWorkspace: boolean; wikiFolderNameSetter?: (value: string) => void; } export default function SearchGithubRepo(props: Props): React.JSX.Element { const userInfos = useUserInfoObservable(); const githubUsername = userInfos?.['github-userName']; const accessToken = userInfos?.['github-token']; const { t } = useTranslation(); const graphqlClient = useMemo( () => new GraphQLClient({ url: GITHUB_GRAPHQL_API, }), [], ); useEffect(() => { graphqlClient.setHeader('Authorization', accessToken === undefined ? '' : `Bearer ${accessToken}`); }, [accessToken, graphqlClient]); if (githubUsername === '' || githubUsername === undefined || accessToken === '' || accessToken === undefined) { return {t('AddWorkspace.WaitForLogin')}; } else { return ( ); } } interface ITokens { accessToken: string; githubUsername: string; } function SearchGithubRepoResultList({ githubWikiUrl, githubWikiUrlSetter, wikiFolderNameSetter, isCreateMainWorkspace, githubUsername, accessToken, }: Props & ITokens): React.JSX.Element { const { t } = useTranslation(); const onSelectRepo = useCallback( (url: string, name: string) => { githubWikiUrlSetter(url); if (typeof wikiFolderNameSetter === 'function') { wikiFolderNameSetter(name); } }, [githubWikiUrlSetter, wikiFolderNameSetter], ); const [githubRepoSearchString, githubRepoSearchStringSetter] = useState('wiki'); const loadCount = 10; // eslint-disable-next-line @typescript-eslint/unbound-method const { loading, error, data, refetch } = useQuery(SEARCH_REPO_QUERY, { variables: { first: loadCount, queryString: `user:${githubUsername} ${githubRepoSearchString}`, login: githubUsername, }, skipCache: true, }); const refetchDebounced = useDebouncedCallback(refetch, [], 300); // clear list on logout, which will cause accessToken change useEffect(() => { const timeoutHandle = setTimeout(async () => { await refetchDebounced(); }, 100); return () => { clearTimeout(timeoutHandle); }; }, [githubUsername, accessToken]); // try refetch on error const [retryInterval, retryIntervalSetter] = useState(100); useEffect(() => { if (error !== undefined && githubUsername.length > 0 && accessToken.length > 0) { const timeoutHandle = setTimeout(async () => { await refetchDebounced(); retryIntervalSetter(retryInterval * 10); }, retryInterval); return () => { clearTimeout(timeoutHandle); }; } return undefined; }, [error, githubUsername, accessToken, retryInterval]); const [createRepository] = useMutation(CREATE_REPO_MUTATION); const repositoryCount = data?.search.repositoryCount; const repoList: IGithubSearchNode[] = useMemo( () => (data !== undefined && (repositoryCount ?? 0) > 0 ? data.search.edges.map(({ node }) => node) : []), [data, repositoryCount], ); // auto select first one after first search useEffect(() => { if (githubWikiUrl.length === 0 && repoList.length > 0) { onSelectRepo(repoList[0].url, repoList[0].name); } }, [repoList, githubWikiUrl, onSelectRepo]); const [isCreatingRepo, isCreatingRepoSetter] = useState(false); const githubUserID = data?.repositoryOwner.id; const wikiUrlToCreate = `https://github.com/${githubUsername ?? '???'}/${githubRepoSearchString}`; const isCreateNewRepo = trim(githubWikiUrl) === wikiUrlToCreate; const githubPagesUrl = `https://${githubUsername ?? '???'}.github.io/${githubRepoSearchString}`; let helperText = ''; if (error !== undefined) { helperText = t('AddWorkspace.CanNotLoadList'); } if (repositoryCount !== undefined && repositoryCount > loadCount) { helperText = t('AddWorkspace.OmitMoreResult', { loadCount }); } return ( ) => { githubRepoSearchStringSetter(event.target.value); }} label={t('AddWorkspace.SearchGithubRepoName')} value={githubRepoSearchString} helperText={helperText} /> {(loading || isCreatingRepo) && } {repoList.map(({ name, url }) => ( { onSelectRepo(url, name); }} selected={trim(githubWikiUrl) === trim(url)} > ))} {error === undefined && !loading && !isCreatingRepo && !repoList.some(({ url }) => trim(url) === wikiUrlToCreate) && githubRepoSearchString !== '' && ( { isCreatingRepoSetter(true); await createRepository({ variables: { repoName: githubRepoSearchString, homePageUrl: isCreateMainWorkspace ? githubPagesUrl : undefined, ownerId: githubUserID, visibility: isCreateMainWorkspace ? 'PUBLIC' : 'PRIVATE', }, }); // wait for Github update their db await Promise.delay(1000); await refetchDebounced(); isCreatingRepoSetter(false); githubWikiUrlSetter(wikiUrlToCreate); }} selected={isCreateNewRepo} > )} {repoList.length === 0 && ( } onClick={() => void refetchDebounced()}> {t('AddWorkspace.Reload')} )} ); }