mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
Github Sync: initial commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import GithubLogo from '@/shared/svgs/github-logo'
|
||||
import IntegrationCard from '@/features/integrations-panel/integration-card'
|
||||
import GitSyncModal from './modals/git-sync-modal'
|
||||
import { GitSyncModalStatus } from '../types/git-sync-types'
|
||||
|
||||
const GitHubSyncCard = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { projectId, name } = useProjectContext()
|
||||
const [modalStatus, setModalStatus] = useState<GitSyncModalStatus>('loading')
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationCard
|
||||
title={t('github')}
|
||||
description={t('sync_with_a_github_repository')}
|
||||
icon={<GithubLogo size={32} />}
|
||||
showPaywallBadge={false}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
</IntegrationCard>
|
||||
<GitSyncModal
|
||||
show={showModal}
|
||||
modalStatus={modalStatus}
|
||||
setModalStatus={setModalStatus}
|
||||
handleHide={() => {
|
||||
setShowModal(false)
|
||||
setModalStatus('loading')
|
||||
}}
|
||||
projectId={projectId}
|
||||
projectName={name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitHubSyncCard
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import GithubLogo from '@/shared/svgs/github-logo'
|
||||
|
||||
export const GitHubSyncWidget = function GitHubSyncWidget() {
|
||||
const { t } = useTranslation()
|
||||
const { appName } = getMeta('ol-ExposedSettings')
|
||||
|
||||
const {
|
||||
isLoading: isCheckingConn,
|
||||
isError: isErrorConnCheck,
|
||||
runAsync: runAsyncConnCheck,
|
||||
data: isConnected,
|
||||
setData: setConnState,
|
||||
} = useAsync<boolean>()
|
||||
|
||||
const {
|
||||
isLoading: isUnlinking,
|
||||
isError: isErrorUnlink,
|
||||
runAsync: runAsyncUnlink,
|
||||
} = useAsync<void>()
|
||||
|
||||
const [showUnlinkModal, setShowUnlinkModal] = useState(false)
|
||||
|
||||
const handleConnCheck = useCallback(() => {
|
||||
runAsyncConnCheck(getJSON('/user/github-sync/status')).catch(err =>
|
||||
debugConsole.error(err?.data?.message || err?.message || err),
|
||||
)
|
||||
}, [runAsyncConnCheck])
|
||||
|
||||
useEffect(() => {
|
||||
handleConnCheck()
|
||||
}, [handleConnCheck])
|
||||
|
||||
const handleUnlink = useCallback(() => {
|
||||
runAsyncUnlink(postJSON('/user/github-sync/unlink'))
|
||||
.then(() => setConnState(false))
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
.finally(() => setShowUnlinkModal(false))
|
||||
}, [runAsyncUnlink])
|
||||
|
||||
if (isCheckingConn) {
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
<div>
|
||||
<GithubLogo />
|
||||
</div>
|
||||
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4>GitHub</h4>
|
||||
</div>
|
||||
|
||||
<p className="small">
|
||||
<span>{t('loading')}…</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="settings-widget-container">
|
||||
<div>
|
||||
<GithubLogo size={40} />
|
||||
</div>
|
||||
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4 id="github-sync">GitHub</h4>
|
||||
</div>
|
||||
|
||||
<p className="small">
|
||||
{t('github_sync_description', { appName })}
|
||||
</p>
|
||||
|
||||
{isErrorConnCheck && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('github_sync_error')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isErrorUnlink && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{isConnected ? (
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={() => setShowUnlinkModal(true)}
|
||||
disabled={isUnlinking}
|
||||
>
|
||||
{isUnlinking ? t('unlinking') : t('unlink')}
|
||||
</OLButton>
|
||||
) : isErrorConnCheck ? (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleConnCheck}
|
||||
>
|
||||
{t('reconnect')}
|
||||
</OLButton>
|
||||
) : (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/github-sync/oauth2"
|
||||
>
|
||||
{t('link')}
|
||||
</OLButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OLModal
|
||||
id="git-sync-modal"
|
||||
show={showUnlinkModal}
|
||||
onHide={() => setShowUnlinkModal(false)}
|
||||
backdrop="static"
|
||||
>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>
|
||||
{t('unlink_provider_account_title', {
|
||||
provider: 'GitHub',
|
||||
})}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('unlink_github_warning', {
|
||||
provider: 'GitHub',
|
||||
})}
|
||||
</p>
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={() => setShowUnlinkModal(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={handleUnlink}
|
||||
disabled={isUnlinking}
|
||||
>
|
||||
{isUnlinking ? t('unlinking') : t('unlink')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
|
||||
|
||||
export default function ImportFromGitHubMenu({ onClick }: { onClick: (e: React.MouseEvent) => void }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<DropdownItem onClick={onClick}>
|
||||
{t('import_from_github')}
|
||||
</DropdownItem>
|
||||
)
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import {
|
||||
postJSON,
|
||||
getJSON,
|
||||
} from '@/infrastructure/fetch-json'
|
||||
import getMeta from '@/utils/meta'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
|
||||
type GitSyncRepo = {
|
||||
name: string
|
||||
fullName: string
|
||||
defaultBranchName: string
|
||||
}
|
||||
|
||||
type GitSyncReposResponse = {
|
||||
repos?: GitSyncRepo[]
|
||||
}
|
||||
|
||||
function ImportFromGitHubModalContent({ handleHide }: { handleHide: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const { appName } = getMeta('ol-ExposedSettings')
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
data,
|
||||
runAsync,
|
||||
} = useAsync<GitSyncReposResponse>()
|
||||
|
||||
useEffect(() => {
|
||||
runAsync(getJSON('/user/github-sync/repos'))
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
}, [runAsync])
|
||||
|
||||
const reposExist = data?.repos != null
|
||||
const repos = reposExist ? data.repos : []
|
||||
|
||||
const {
|
||||
isLoading: isImporting,
|
||||
isSuccess: isImported,
|
||||
isError: isErrorImport,
|
||||
runAsync: runAsyncImport,
|
||||
} = useAsync<{ projectId: string }>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isImported) {
|
||||
handleHide()
|
||||
}
|
||||
}, [handleHide, isImported])
|
||||
|
||||
const showLinkToGitHub = !isImporting && isSuccess && !reposExist
|
||||
const showRepos = !isImporting && isSuccess && reposExist
|
||||
|
||||
const handleImport = (repo: GitSyncRepo) => {
|
||||
|
||||
runAsyncImport(
|
||||
postJSON('/project/new/github-sync', {
|
||||
body: repo
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
window.location.href = `/project/${data.projectId}`
|
||||
})
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalHeader onClose={handleHide}>
|
||||
<OLModalTitle>{t('import_from_github')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
{isLoading && (
|
||||
<span>
|
||||
<OLSpinner size="sm" className="me-2"/>
|
||||
{t('loading_github_repositories')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isImporting && (
|
||||
<span>
|
||||
<OLSpinner size="sm" className="me-2"/>
|
||||
{t('importing')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="notification-list">
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('something_went_wrong_server')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLinkToGitHub && (
|
||||
<div className="text-center">
|
||||
<p>
|
||||
{t('link_to_github_description', { appName })}
|
||||
</p>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/github-sync/oauth2"
|
||||
>
|
||||
{t('link_to_github')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRepos && (
|
||||
<div>
|
||||
{repos.length === 0 ? (
|
||||
<p className="text-center">
|
||||
{t('you_dont_have_any_repositories')}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-center">
|
||||
{t('select_github_repository', { appName })}
|
||||
</p>
|
||||
<div className="table-container table-container-bordered">
|
||||
<table className="table table-striped table-hover">
|
||||
<tbody>
|
||||
{repos.map(repo => (
|
||||
<tr key={repo.fullName}>
|
||||
<td>
|
||||
{repo.name}
|
||||
<div className="small">
|
||||
<a
|
||||
href={`https://github.com/${repo.fullName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{repo.fullName}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={() => handleImport(repo)}
|
||||
>
|
||||
{t('import_to_sharelatex', { appName: 'Overleaf' })}
|
||||
</OLButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isErrorImport && (
|
||||
<div className="notification-list">
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('something_went_wrong_server')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
{!isImporting && (
|
||||
<span className="me-auto">
|
||||
<a
|
||||
href="https://help.github.com/en/articles/requesting-organization-approval-for-oauth-apps"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('cant_see_what_youre_looking_for_question')}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
disabled={isImporting}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ImportFromGitHubModal({ onHide }: { onHide: () => void }) {
|
||||
return (
|
||||
<OLModal
|
||||
id="git-import-modal"
|
||||
show
|
||||
animation
|
||||
size="lg"
|
||||
onHide={onHide}
|
||||
backdrop="static"
|
||||
>
|
||||
<ImportFromGitHubModalContent handleHide={onHide} />
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { ProjectSyncState } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncNeedPermissionModalProps = {
|
||||
projectSyncState: ProjectSyncState
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
const GitSyncCannotExportModal = ({ projectSyncState, handleHide }: GitSyncCannotExportModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={(
|
||||
<Trans
|
||||
i18nKey="ask_proj_owner_to_export_to_github"
|
||||
values={{
|
||||
projectOwnerEmail: projectSyncState.ownerEmail ?? '?',
|
||||
}}
|
||||
components={[
|
||||
projectSyncState.ownerEmail ? <a href={`mailto:${projectSyncState.ownerEmail}`} /> : <></>
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncCannotExportModal
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { deleteJSON } from '@/infrastructure/fetch-json'
|
||||
import { OLModalBody, OLModalFooter } from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { ProjectSyncState, GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncUnlinkModalProps = {
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
projectSyncState: ProjectSyncState
|
||||
projectId: string
|
||||
}
|
||||
|
||||
const GitSyncUnlinkModal = ({
|
||||
handleHide,
|
||||
setModalStatus,
|
||||
projectSyncState,
|
||||
projectId,
|
||||
}: GitSyncInitModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
error,
|
||||
isLoading,
|
||||
runAsync,
|
||||
} = useAsync<void>()
|
||||
|
||||
const handleUnlink = () => {
|
||||
runAsync(deleteJSON(`/project/${projectId}/github-sync`))
|
||||
.then(() => setModalStatus('need-export'))
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('project_linked_to')}:
|
||||
<a
|
||||
href={`https://github.com/${projectSyncState.repoFullName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{projectSyncState.repoFullName}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
{!error ? (
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={t('unlink_the_project_from_the_current_github_repo')}
|
||||
/>
|
||||
) : (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={
|
||||
error.info?.statusCode === 403 ? (
|
||||
<Trans
|
||||
i18nKey="ask_proj_owner_to_unlink_from_current_github"
|
||||
values={{ projectOwnerEmail: error?.data?.ownerEmail ?? '?' }}
|
||||
components={[
|
||||
error?.data?.ownerEmail ? <a href={`mailto:${error.data.ownerEmail}`} /> : <></>
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
t('something_went_wrong_server')
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</OLModalBody>
|
||||
|
||||
|
||||
<OLModalFooter>
|
||||
<div className="d-flex gap-2">
|
||||
<OLButton
|
||||
variant="danger"
|
||||
onClick={handleUnlink}
|
||||
disabled={isLoading || !!error}
|
||||
>
|
||||
{t('confirm')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
</div>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncUnlinkModal
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
|
||||
import { ProjectSyncState, GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncConflictModalProps = {
|
||||
projectSyncState: ProjectSyncState
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
}
|
||||
|
||||
const GitSyncConflictModal = ({ projectSyncState, handleHide, setModalStatus }: GitSyncConflictModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { appName } = getMeta('ol-ExposedSettings')
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={t('github_merge_failed_error', { appName })}
|
||||
/>
|
||||
<p className="mt-2">
|
||||
<Trans
|
||||
i18nKey="github_manual_merge_user_prompt"
|
||||
values={{sharelatex_branch: projectSyncState.unmergedBranchName }}
|
||||
components={[<b />]}
|
||||
/>
|
||||
</p>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={
|
||||
() => setModalStatus('run-merge-resolved')
|
||||
}
|
||||
>
|
||||
{t('continue_github_merge')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncConflictModal
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useEffect } from 'react'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import {
|
||||
getJSON,
|
||||
postJSON
|
||||
} from '@/infrastructure/fetch-json'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLForm from '@/shared/components/ol/ol-form'
|
||||
import OLFormGroup from '@/shared/components/ol/ol-form-group'
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import OLFormLabel from '@/shared/components/ol/ol-form-label'
|
||||
import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
|
||||
import OLFormSelect from '@/shared/components/ol/ol-form-select'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import { GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type OrgsResponse = {
|
||||
user: string
|
||||
orgs: string[]
|
||||
}
|
||||
|
||||
type GitSyncExportModalProps = {
|
||||
projectId: string
|
||||
projectName: string
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
}
|
||||
|
||||
const GitSyncExportModal = ({
|
||||
projectId,
|
||||
projectName,
|
||||
handleHide,
|
||||
setModalStatus
|
||||
}: GitSyncExportModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [selectedOwner, setSelectedOwner] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [repoName, setRepoName] = useState(projectName)
|
||||
const [description, setDescription] = useState('')
|
||||
const [visibility, setVisibility] = useState<'public' | 'private'>('private')
|
||||
|
||||
const {
|
||||
runAsync: runAsyncUserAndOrgs,
|
||||
data: userAndOrgs,
|
||||
error: errorUserAndOrgs
|
||||
} = useAsync<OrgsResponse>()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncUserAndOrgs(getJSON('/user/github-sync/orgs'))
|
||||
.then(userAndOrgs => setSelectedOwner(userAndOrgs?.user))
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
}, [])
|
||||
|
||||
const { isLoading, error, setError, runAsync } = useAsync<void>()
|
||||
|
||||
const createRepo = () => {
|
||||
const isPublic = visibility === 'public'
|
||||
const org = selectedOwner === userAndOrgs?.user ? undefined : selectedOwner
|
||||
|
||||
runAsync(postJSON(`/project/${projectId}/github-sync/export`, {
|
||||
body: {
|
||||
name: repoName,
|
||||
description,
|
||||
isPublic,
|
||||
org,
|
||||
},
|
||||
}))
|
||||
.then(() => setModalStatus('loading'))
|
||||
.catch(err => {
|
||||
debugConsole.error(err?.data?.message || err?.message || err)
|
||||
if (!err?.data?.key) setError(t('something_went_wrong_server'))
|
||||
else setError(t(err.data.key))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<h4>{t('export_project_to_github')}</h4>
|
||||
<p>{t('project_not_linked_to_github')}</p>
|
||||
|
||||
{error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{errorUserAndOrgs && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('something_went_wrong_server')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OLForm onSubmit={createRepo}>
|
||||
<OLRow>
|
||||
<OLCol xs={4}>
|
||||
<OLFormGroup>
|
||||
<OLFormLabel htmlFor="github-sync-owner">
|
||||
{selectedOwner === userAndOrgs?.user
|
||||
? t('owner')
|
||||
: t('organization')}
|
||||
</OLFormLabel>
|
||||
<OLFormSelect
|
||||
id="github-sync-owner"
|
||||
name="org"
|
||||
value={selectedOwner}
|
||||
onChange={e => setSelectedOwner(e.target.value)}
|
||||
>
|
||||
<option key={userAndOrgs?.user} value={userAndOrgs?.user}>
|
||||
{userAndOrgs?.user}
|
||||
</option>
|
||||
{userAndOrgs?.orgs.map(org => (
|
||||
<option key={org} value={org}>
|
||||
{org}
|
||||
</option>
|
||||
))}
|
||||
</OLFormSelect>
|
||||
</OLFormGroup>
|
||||
</OLCol>
|
||||
|
||||
<OLCol xs={5}>
|
||||
<OLFormGroup>
|
||||
<OLFormLabel htmlFor="github-sync-name">
|
||||
{t('repository_name')}
|
||||
</OLFormLabel>
|
||||
<OLFormControl
|
||||
id="github-sync-name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={repoName}
|
||||
onChange={e => setRepoName(e.target.value)}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
<OLRow>
|
||||
<OLCol xs={12}>
|
||||
<OLFormGroup>
|
||||
<OLFormLabel htmlFor="github-sync-description">
|
||||
{t('description')} ({t('optional')})
|
||||
</OLFormLabel>
|
||||
<OLFormControl
|
||||
id="github-sync-description"
|
||||
name="description"
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
<hr />
|
||||
|
||||
<fieldset>
|
||||
<legend className="visually-hidden">
|
||||
{t('repository_visibility')}
|
||||
</legend>
|
||||
|
||||
<OLFormGroup>
|
||||
<OLRow>
|
||||
<OLCol xs={12}>
|
||||
<OLFormCheckbox
|
||||
type="radio"
|
||||
id="public"
|
||||
name="repository"
|
||||
value="public"
|
||||
checked={visibility === 'public'}
|
||||
onChange={() => setVisibility('public')}
|
||||
label={t('public', { defaultValue: 'Public' })}
|
||||
description={t('github_public_description')}
|
||||
/>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
<OLRow>
|
||||
<OLCol xs={12}>
|
||||
<OLFormCheckbox
|
||||
className="mr-1"
|
||||
type="radio"
|
||||
id="private"
|
||||
name="repository"
|
||||
value="private"
|
||||
checked={visibility === 'private'}
|
||||
onChange={() => setVisibility('private')}
|
||||
label={t('private', { defaultValue: 'Private' })}
|
||||
description={t('github_private_description')}
|
||||
/>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</OLFormGroup>
|
||||
</fieldset>
|
||||
</OLForm>
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={createRepo}
|
||||
disabled={!repoName.trim() || isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('create_project_in_github')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncExportModal
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncLoadingModalProps = {
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
errorMessage: string | null
|
||||
}
|
||||
const GitSyncLoadingModal = ({ handleHide, setModalStatus, errorMessage }: GitSyncLoadingModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<div role="status" className="loading align-items-start">
|
||||
<div aria-hidden="true" data-testid="ol-spinner" className="spinner-border spinner-border-sm"></div>
|
||||
{t('checking_project_github_status')}
|
||||
</div>
|
||||
</OLModalBody>
|
||||
|
||||
{errorMessage && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OLModalFooter>
|
||||
{errorMessage && (
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={() => setModalStatus('confirm-unlink')}
|
||||
>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
)}
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncLoadingModal
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { GitSyncModalStatus, ProjectSyncState } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncMergeModalProps = {
|
||||
handleHide: () => void
|
||||
modalStatus: GitSyncModalStatus
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
setProjectSyncState: (projectSyncState: ProjectSyncState) => void
|
||||
commitMessage: string
|
||||
setCommitMessage: (message: string) => void
|
||||
projectId: string
|
||||
}
|
||||
|
||||
const GitSyncMergeModal = ({
|
||||
handleHide,
|
||||
modalStatus,
|
||||
setModalStatus,
|
||||
setProjectSyncState,
|
||||
commitMessage,
|
||||
setCommitMessage,
|
||||
projectId
|
||||
}: GitSynMergeModalProps) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { error, isError, isLoading, setError, runAsync } = useAsync<ProjectSyncState>()
|
||||
|
||||
useEffect(() => {
|
||||
if (modalStatus !== 'run-merge' &&
|
||||
modalStatus !== 'run-merge-resolved'
|
||||
) return
|
||||
|
||||
const claimConflictIsResolved = (modalStatus === 'run-merge-resolved')
|
||||
|
||||
runAsync(
|
||||
postJSON(`/project/${projectId}/github-sync/merge`, {
|
||||
body: { message: commitMessage, claimConflictIsResolved },
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
switch (data.mergeStatus) {
|
||||
|
||||
case 'clean':
|
||||
setModalStatus('merge-overview')
|
||||
break
|
||||
|
||||
case 'conflict':
|
||||
setModalStatus('show-conflict')
|
||||
break
|
||||
|
||||
default:
|
||||
const message = `Unexpected merge status received: ${data.mergeStatus}`
|
||||
const msError = new Error(message)
|
||||
setError(msError)
|
||||
throw msError
|
||||
}
|
||||
setProjectSyncState(data)
|
||||
setCommitMessage('')
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error(err?.data?.message || err?.message || err)
|
||||
if (err?.info?.statusCode === 403 || err?.info?.statusCode === 404) setModalStatus('loading')
|
||||
})
|
||||
|
||||
}, [
|
||||
modalStatus,
|
||||
projectId,
|
||||
commitMessage,
|
||||
runAsync,
|
||||
])
|
||||
|
||||
if (!isLoading && !isError) return
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<div
|
||||
role="status"
|
||||
className="loading align-items-start"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-testid="ol-spinner"
|
||||
className="spinner-border spinner-border-sm"
|
||||
></div>
|
||||
|
||||
{t('importing_and_merging_changes_in_github')}
|
||||
</div>
|
||||
</OLModalBody>
|
||||
|
||||
{error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncMergeModal
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { getJSON } from '@/infrastructure/fetch-json'
|
||||
import { OLModalBody, OLModalFooter } from '@/shared/components/ol/ol-modal'
|
||||
import OLForm from '@/shared/components/ol/ol-form'
|
||||
import OLFormGroup from '@/shared/components/ol/ol-form-group'
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLIconButton from '@/shared/components/ol/ol-icon-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { ProjectSyncState, GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncMergeOverviewModalProps = {
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
projectSyncState: ProjectSyncState
|
||||
projectId: string
|
||||
commitMessage: string
|
||||
setCommitMessage: (message: string) => void
|
||||
|
||||
}
|
||||
|
||||
type GitCommit = {
|
||||
message: string
|
||||
author: {
|
||||
name: string
|
||||
email: string
|
||||
date: string
|
||||
}
|
||||
sha: string
|
||||
}
|
||||
|
||||
type UnmergedCommitsResponse = {
|
||||
commits: GitCommit[]
|
||||
diverged: boolean
|
||||
isProjectUpdated: boolean
|
||||
}
|
||||
|
||||
const GitSyncMergeOverviewModal = ({
|
||||
handleHide,
|
||||
setModalStatus,
|
||||
projectSyncState,
|
||||
projectId,
|
||||
commitMessage,
|
||||
setCommitMessage
|
||||
}: GitSyncMergeOverviewModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const appName = 'Overleaf'
|
||||
|
||||
const {
|
||||
data,
|
||||
setData,
|
||||
error,
|
||||
isSuccess,
|
||||
isLoading,
|
||||
runAsync,
|
||||
} = useAsync<UnmergedCommitsResponse | null>()
|
||||
|
||||
const loadUnmergedCommits = () => {
|
||||
setData({})
|
||||
|
||||
runAsync(getJSON(`/project/${projectId}/github-sync/merge/overview`))
|
||||
.then(data => { if (!data) setModalStatus('loading') })
|
||||
.catch(err => {
|
||||
debugConsole.error(err?.data?.message || err?.message || err)
|
||||
if (err?.info?.statusCode === 403 ||
|
||||
err?.info?.statusCode === 404 ||
|
||||
err?.info?.statusCode === 401
|
||||
) setModalStatus('loading')
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUnmergedCommits()
|
||||
}, [projectId, runAsync])
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('project_linked_to')}:
|
||||
<a
|
||||
href={`https://github.com/${projectSyncState.repoFullName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{projectSyncState.repoFullName}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{isLoading && (
|
||||
<>
|
||||
<div role="status" className="loading align-items-start">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-testid="ol-spinner"
|
||||
className="spinner-border spinner-border-sm"
|
||||
></div>
|
||||
{t('loading')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
{data.diverged && (
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={t('github_repository_diverged')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h3 className="github-sync-commits-heading">
|
||||
{t('recent_commits_in_github')}
|
||||
|
||||
<OLIconButton
|
||||
icon="refresh"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={loadUnmergedCommits}
|
||||
accessibilityLabel={t('refresh')}
|
||||
/>
|
||||
</h3>
|
||||
|
||||
{data.commits?.length === 0 && (
|
||||
<div>
|
||||
<p className="small">{t('no_new_commits_in_github')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.commits?.length > 0 && (
|
||||
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||
{data.commits.map((commit: GitCommit) => (
|
||||
<div key={commit.sha}>
|
||||
<span className="small float-end">
|
||||
<a
|
||||
href={`https://github.com/${projectSyncState.repoFullName}/commit/${commit.sha}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{commit.sha.substring(0, 7)}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<a
|
||||
href={`https://github.com/${projectSyncState.repoFullName}/commit/${commit.sha}`}
|
||||
target="_blank"
|
||||
className="commit-message"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{commit.message}
|
||||
</a>
|
||||
|
||||
<div className="small">
|
||||
by {commit.author.name} <{commit.author.email}>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.isProjectUpdated && (
|
||||
<>
|
||||
<hr />
|
||||
<OLForm>
|
||||
<p>{t('sync_project_to_github_explanation', { appName })}</p>
|
||||
<OLRow>
|
||||
<OLCol xs={12}>
|
||||
<OLFormGroup>
|
||||
<OLFormControl
|
||||
as="textarea"
|
||||
rows={1}
|
||||
value={commitMessage}
|
||||
placeholder={t('github_commit_message_placeholder', { appName })}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</OLForm>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
)}
|
||||
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<div className="d-flex justify-content-between w-100">
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={() => setModalStatus('confirm-unlink')}
|
||||
disabled={!isSuccess && error?.info?.statusCode !== 404}
|
||||
>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
leadingIcon="sync"
|
||||
onClick={() => setModalStatus('run-merge')}
|
||||
disabled={!isSuccess}
|
||||
>
|
||||
{t('sync')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncMergeOverviewModal
|
||||
@@ -0,0 +1,226 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
OLModal,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
|
||||
import { getJSON } from '@/infrastructure/fetch-json'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
import GitSyncLoadingModal from './git-sync-loading-modal'
|
||||
import GitSyncMergeOverviewModal from './git-sync-merge-overview-modal'
|
||||
import GitSyncMergeModal from './git-sync-merge-modal'
|
||||
import GitSyncConflictModal from './git-sync-conflict-modal'
|
||||
import GitSyncExportModal from './git-sync-export-modal'
|
||||
import GitSyncNeedAuthModal from './git-sync-need-auth-modal'
|
||||
import GitSyncNeedPermissionModal from './git-sync-need-permission-modal'
|
||||
import GitSyncConfirmUnlinkModal from './git-sync-confirm-unlink-modal'
|
||||
import GitSyncUnlinkUnavailableModal from './git-sync-unlink-unavailable-modal'
|
||||
import GitSyncCannotExportModal from './git-sync-cannot-export-modal'
|
||||
|
||||
import { GitSyncModalStatus, ProjectSyncState } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncModalProps = {
|
||||
show: boolean
|
||||
handleHide: () => void
|
||||
projectId: string
|
||||
projectName: string
|
||||
modalStatus: GitSyncModalStatus
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
}
|
||||
|
||||
function GitSyncModal({
|
||||
show,
|
||||
handleHide,
|
||||
projectId,
|
||||
projectName,
|
||||
modalStatus,
|
||||
setModalStatus,
|
||||
}: GitSyncModalProps) {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [commitMessage, setCommitMessage] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
const {
|
||||
runAsync: runAsyncConn,
|
||||
error: errorConn,
|
||||
setError: setErrorConn,
|
||||
data: dataConn,
|
||||
} = useAsync<boolean>()
|
||||
|
||||
const {
|
||||
runAsync,
|
||||
data: projectSyncState,
|
||||
setData: setProjectSyncState,
|
||||
error,
|
||||
setError,
|
||||
} = useAsync<ProjectSyncState>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!show) return
|
||||
if (modalStatus !== 'loading') return
|
||||
|
||||
const fetchProjectState = () => {
|
||||
setError(null)
|
||||
setErrorMessage('')
|
||||
|
||||
runAsync(getJSON(`/project/${projectId}/github-sync/state`))
|
||||
.then(pss => {
|
||||
switch (pss.mergeStatus) {
|
||||
// pss.ownerEmail is set to the email address of the project owner
|
||||
// or null if the user is the project owner
|
||||
// only owner can export project to git server
|
||||
case 'need-export':
|
||||
if (!pss.ownerEmail) setModalStatus('need-export')
|
||||
else setModalStatus('cannot-export')
|
||||
break
|
||||
|
||||
case 'clean':
|
||||
case 'diverged':
|
||||
setModalStatus('merge-overview')
|
||||
break
|
||||
|
||||
case 'conflict':
|
||||
setModalStatus('show-conflict')
|
||||
break
|
||||
|
||||
// user does not have push permission to repo
|
||||
// pss.ownerEmail is set to the email address of the owner
|
||||
// or null if the user is the project owner
|
||||
// if the user is owner, it seems that repo is not accessible
|
||||
case 'need-permission': {
|
||||
if (!pss.ownerEmail) setModalStatus('unlink-unavailable')
|
||||
else setModalStatus('need-permission')
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error(err?.data?.message || err?.message || err)
|
||||
setErrorMessage(t('generic_something_went_wrong'))
|
||||
})
|
||||
}
|
||||
|
||||
const fetchConnectionStatus = () => {
|
||||
setErrorConn(null)
|
||||
setErrorMessage('')
|
||||
|
||||
runAsyncConn(getJSON(`/user/github-sync/status`))
|
||||
.then(isConnected => {
|
||||
if (!isConnected) {
|
||||
setModalStatus('need-auth')
|
||||
return
|
||||
}
|
||||
fetchProjectState()
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error(err?.data?.message || err?.message || err)
|
||||
setErrorMessage(t('generic_something_went_wrong'))
|
||||
})
|
||||
}
|
||||
|
||||
fetchConnectionStatus()
|
||||
|
||||
}, [show, modalStatus, projectId, runAsyncConn, runAsync])
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={handleHide} backdrop="static">
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('sync_with_github')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
{modalStatus === 'loading' && (
|
||||
<GitSyncLoadingModal
|
||||
handleHide={handleHide}
|
||||
setModalStatus={setModalStatus}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'need-export' && (
|
||||
<GitSyncExportModal
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
handleHide={handleHide}
|
||||
setModalStatus={setModalStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'cannot-export' && (
|
||||
<GitSyncCannotExportModal
|
||||
projectSyncState={projectSyncState}
|
||||
handleHide={handleHide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'need-auth' && (
|
||||
<GitSyncNeedAuthModal
|
||||
handleHide={handleHide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'need-permission' && (
|
||||
<GitSyncNeedPermissionModal
|
||||
handleHide={handleHide}
|
||||
projectSyncState={projectSyncState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'merge-overview' && (
|
||||
<GitSyncMergeOverviewModal
|
||||
projectId={projectId}
|
||||
projectSyncState={projectSyncState}
|
||||
setModalStatus={setModalStatus}
|
||||
handleHide={handleHide}
|
||||
commitMessage={commitMessage}
|
||||
setCommitMessage={setCommitMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'confirm-unlink' && (
|
||||
<GitSyncConfirmUnlinkModal
|
||||
projectId={projectId}
|
||||
projectSyncState={projectSyncState}
|
||||
setModalStatus={setModalStatus}
|
||||
handleHide={handleHide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'unlink-unavailable' && (
|
||||
<GitSyncUnlinkUnavailableModal
|
||||
projectId={projectId}
|
||||
setModalStatus={setModalStatus}
|
||||
handleHide={handleHide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(modalStatus === 'run-merge' ||
|
||||
modalStatus === 'run-merge-resolved') && (
|
||||
<GitSyncMergeModal
|
||||
handleHide={handleHide}
|
||||
modalStatus={modalStatus}
|
||||
setModalStatus={setModalStatus}
|
||||
commitMessage={commitMessage}
|
||||
setCommitMessage={setCommitMessage}
|
||||
projectId={projectId}
|
||||
setProjectSyncState={setProjectSyncState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalStatus === 'show-conflict' && (
|
||||
<GitSyncConflictModal
|
||||
projectSyncState={projectSyncState}
|
||||
handleHide={handleHide}
|
||||
setModalStatus={setModalStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncModal
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
|
||||
const GitSyncNeedAuthModal = ({ handleHide }: { handleHide: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
const { appName } = getMeta('ol-ExposedSettings')
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<p>{t('link_to_github_description', { appName })}</p>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
handleHide()
|
||||
window.open(
|
||||
'/user/github-sync/oauth2',
|
||||
'githubAuth',
|
||||
'width=600,height=700'
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('link_to_github')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncNeedAuthModal
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { ProjectSyncState } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncNeedPermissionModalProps = {
|
||||
projectSyncState: ProjectSyncState
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
const GitSyncNeedPermissionModal = ({ projectSyncState, handleHide }: GitSyncConflictModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={(
|
||||
<Trans
|
||||
i18nKey="ask_proj_owner_to_add_you_as_github_collaborator"
|
||||
values={{
|
||||
repoFullName: projectSyncState.repoFullName ?? '?',
|
||||
projectOwnerEmail: projectSyncState.ownerEmail ?? '?',
|
||||
}}
|
||||
components={[
|
||||
projectSyncState.repoFullName ? (
|
||||
<a
|
||||
href={`https://github.com/${projectSyncState.repoFullName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
projectSyncState.ownerEmail ? <a href={`mailto:${projectSyncState.ownerEmail}`} /> : <></>
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncNeedPermissionModal
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { deleteJSON } from '@/infrastructure/fetch-json'
|
||||
import { OLModalBody, OLModalFooter } from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { GitSyncModalStatus } from '../../types/git-sync-types'
|
||||
|
||||
type GitSyncUnlinkUnavailableModalProps = {
|
||||
handleHide: () => void
|
||||
setModalStatus: (modalStatus: GitSyncModalStatus) => void
|
||||
projectId: string
|
||||
}
|
||||
|
||||
const GitSyncUnlinkUnavailableModal = ({
|
||||
handleHide,
|
||||
setModalStatus,
|
||||
projectId,
|
||||
}: GitSyncInitModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
error,
|
||||
isLoading,
|
||||
runAsync,
|
||||
} = useAsync<void>()
|
||||
|
||||
const handleUnlink = () => {
|
||||
runAsync(deleteJSON(`/project/${projectId}/github-sync`))
|
||||
.then(() => setModalStatus('need-export'))
|
||||
.catch(err => debugConsole.error(err?.data?.message || err?.message || err))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLModalBody>
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('github_sync_repository_not_found_description')}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={
|
||||
error.info?.statusCode === 403 ? (
|
||||
<Trans
|
||||
i18nKey="ask_proj_owner_to_unlink_from_current_github"
|
||||
values={{ projectOwnerEmail: error?.data?.ownerEmail ?? '?' }}
|
||||
components={[
|
||||
error?.data?.ownerEmail ? <a href={`mailto:${error.data.ownerEmail}`} /> : <></>
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
t('something_went_wrong_server')
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<div className="d-flex gap-2">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleUnlink}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('unlink_github_repository')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
</div>
|
||||
</OLModalFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitSyncUnlinkUnavailableModal
|
||||
@@ -0,0 +1,19 @@
|
||||
export type GitSyncModalStatus =
|
||||
'loading'
|
||||
| 'need-export'
|
||||
| 'cannot-export'
|
||||
| 'need-auth'
|
||||
| 'need-permission'
|
||||
| 'merge-overview'
|
||||
| 'confirm-unlink'
|
||||
| 'unlink-unavailable'
|
||||
| 'run-merge'
|
||||
| 'run-merge-resolved'
|
||||
| 'show-conflict'
|
||||
|
||||
export type ProjectSyncState = {
|
||||
mergeStatus: 'clean' | 'conflict' | 'diverged' | 'need-export' | 'need-permission'
|
||||
repoFullName: string
|
||||
unmergedBranchName: string | null
|
||||
ownerEmail?: string
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.github-sync-commits-heading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-03);
|
||||
font-weight: 600;
|
||||
justify-content: space-between;
|
||||
line-height: var(--line-height-03);
|
||||
}
|
||||
Reference in New Issue
Block a user