mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Merge pull request #18170 from overleaf/ae-token-access-page
Convert token access page to React GitOrigin-RevId: d7434f0de395c47a95d00767727fbe9d43f9abca
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"accept_or_reject_each_changes_individually": "",
|
||||
"accept_terms_and_conditions": "",
|
||||
"accepted_invite": "",
|
||||
"accepting_invite_as": "",
|
||||
"access_denied": "",
|
||||
"account_has_been_link_to_institution_account": "",
|
||||
"account_has_past_due_invoice_change_plan_warning": "",
|
||||
@@ -532,6 +533,7 @@
|
||||
"history_view_all": "",
|
||||
"history_view_labels": "",
|
||||
"hit_enter_to_reply": "",
|
||||
"home": "",
|
||||
"hotkey_add_a_comment": "",
|
||||
"hotkey_autocomplete_menu": "",
|
||||
"hotkey_beginning_of_document": "",
|
||||
@@ -619,6 +621,7 @@
|
||||
"invite_not_accepted": "",
|
||||
"invited_to_group": "",
|
||||
"invited_to_group_have_individual_subcription": "",
|
||||
"invited_to_join": "",
|
||||
"ip_address": "",
|
||||
"is_email_affiliated": "",
|
||||
"issued_on": "",
|
||||
@@ -1335,6 +1338,7 @@
|
||||
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "",
|
||||
"toggle_compile_options_menu": "",
|
||||
"token": "",
|
||||
"token_access_failure": "",
|
||||
"token_limit_reached": "",
|
||||
"token_read_only": "",
|
||||
"token_read_write": "",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const AccessAttemptScreen: FC<{
|
||||
loadingScreenBrandHeight: string
|
||||
inflight: boolean
|
||||
accessError: string | boolean
|
||||
}> = ({ loadingScreenBrandHeight, inflight, accessError }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="loading-screen">
|
||||
<div className="loading-screen-brand-container">
|
||||
<div
|
||||
className="loading-screen-brand"
|
||||
style={{ height: loadingScreenBrandHeight }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="loading-screen-label text-center">
|
||||
{t('join_project')}
|
||||
{inflight && <LoadingScreenEllipses />}
|
||||
</h3>
|
||||
|
||||
{accessError && (
|
||||
<div className="global-alerts text-center">
|
||||
<div>
|
||||
<br />
|
||||
{accessError === 'not_found' ? (
|
||||
<div>
|
||||
<h4 aria-live="assertive">Project not found</h4>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="alert alert-danger" aria-live="assertive">
|
||||
{t('token_access_failure')}
|
||||
</div>
|
||||
<p>
|
||||
<a href="/">{t('home')}</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const LoadingScreenEllipses = () => (
|
||||
<span aria-hidden>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
</span>
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export type RequireAcceptData = {
|
||||
projectName?: string
|
||||
}
|
||||
|
||||
export const RequireAcceptScreen: FC<{
|
||||
requireAcceptData: RequireAcceptData
|
||||
sendPostRequest: (confirmedByUser: boolean) => void
|
||||
}> = ({ requireAcceptData, sendPostRequest }) => {
|
||||
const { t } = useTranslation()
|
||||
const user = getMeta('ol-user')
|
||||
|
||||
return (
|
||||
<div className="loading-screen">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="card">
|
||||
<div className="page-header text-centered">
|
||||
<h1>
|
||||
{t('invited_to_join')}
|
||||
<br />
|
||||
<em>{requireAcceptData.projectName || 'This project'}</em>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{user && (
|
||||
<div className="row text-center">
|
||||
<div className="col-md-12">
|
||||
<p>
|
||||
{t('accepting_invite_as')} <em>{user.email}</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row text-center">
|
||||
<div className="col-md-12">
|
||||
<button
|
||||
className="btn btn-lg btn-primary"
|
||||
type="submit"
|
||||
onClick={() => sendPostRequest(true)}
|
||||
>
|
||||
{t('join_project')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import {
|
||||
V1ImportData,
|
||||
V1ImportDataScreen,
|
||||
} from '@/features/token-access/components/v1-import-data-screen'
|
||||
import { AccessAttemptScreen } from '@/features/token-access/components/access-attempt-screen'
|
||||
import {
|
||||
RequireAcceptData,
|
||||
RequireAcceptScreen,
|
||||
} from '@/features/token-access/components/require-accept-screen'
|
||||
import Icon from '@/shared/components/icon'
|
||||
|
||||
type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
|
||||
|
||||
function TokenAccessRoot() {
|
||||
const [mode, setMode] = useState<Mode>('access-attempt')
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const [accessError, setAccessError] = useState<string | boolean>(false)
|
||||
const [v1ImportData, setV1ImportData] = useState<V1ImportData>()
|
||||
const [requireAcceptData, setRequireAcceptData] =
|
||||
useState<RequireAcceptData>()
|
||||
const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] =
|
||||
useState('0px')
|
||||
const location = useLocation()
|
||||
|
||||
const sendPostRequest = useCallback(
|
||||
(confirmedByUser = false) => {
|
||||
setInflight(true)
|
||||
|
||||
postJSON(getMeta('ol-postUrl'), {
|
||||
body: {
|
||||
confirmedByUser,
|
||||
tokenHashPrefix: document.location.hash,
|
||||
},
|
||||
})
|
||||
.then(async data => {
|
||||
setAccessError(false)
|
||||
|
||||
if (data.redirect) {
|
||||
location.replace(data.redirect)
|
||||
} else if (data.v1Import) {
|
||||
setMode('v1Import')
|
||||
setV1ImportData(data.v1Import)
|
||||
} else if (data.requireAccept) {
|
||||
setMode('requireAccept')
|
||||
setRequireAcceptData(data.requireAccept)
|
||||
} else {
|
||||
debugConsole.warn(
|
||||
'invalid data from server in success response',
|
||||
data
|
||||
)
|
||||
setAccessError(true)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
debugConsole.warn('error response from server', error)
|
||||
setAccessError(error.response?.status === 404 ? 'not_found' : 'error')
|
||||
})
|
||||
.finally(() => {
|
||||
setInflight(false)
|
||||
})
|
||||
},
|
||||
[location]
|
||||
)
|
||||
|
||||
const postedRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!postedRef.current) {
|
||||
postedRef.current = true
|
||||
sendPostRequest()
|
||||
setTimeout(() => {
|
||||
setLoadingScreenBrandHeight('20%')
|
||||
}, 500)
|
||||
}
|
||||
}, [sendPostRequest])
|
||||
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="full-size">
|
||||
<div>
|
||||
<a
|
||||
href="/project"
|
||||
// TODO: class name
|
||||
style={{ fontSize: '2rem', marginLeft: '1rem', color: '#ddd' }}
|
||||
>
|
||||
<Icon type="arrow-left" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{mode === 'access-attempt' && (
|
||||
<AccessAttemptScreen
|
||||
accessError={accessError}
|
||||
inflight={inflight}
|
||||
loadingScreenBrandHeight={loadingScreenBrandHeight}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'v1Import' && v1ImportData && (
|
||||
<V1ImportDataScreen v1ImportData={v1ImportData} />
|
||||
)}
|
||||
|
||||
{mode === 'requireAccept' && requireAcceptData && (
|
||||
<RequireAcceptScreen
|
||||
requireAcceptData={requireAcceptData}
|
||||
sendPostRequest={sendPostRequest}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)
|
||||
@@ -0,0 +1,84 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
export type V1ImportData = {
|
||||
name?: string
|
||||
status: string
|
||||
projectId: string
|
||||
}
|
||||
export const V1ImportDataScreen: FC<{ v1ImportData: V1ImportData }> = ({
|
||||
v1ImportData,
|
||||
}) => {
|
||||
return (
|
||||
<div className="loading-screen">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-sm-8 col-sm-offset-2">
|
||||
<h1 className="text-center">
|
||||
{v1ImportData.status === 'mustLogin'
|
||||
? 'Please log in'
|
||||
: 'Overleaf v1 Project'}
|
||||
</h1>
|
||||
|
||||
<img
|
||||
className="v2-import__img"
|
||||
src="/img/v1-import/v2-editor.png"
|
||||
alt="The new V2 editor."
|
||||
/>
|
||||
|
||||
{v1ImportData.status === 'cannotImport' && (
|
||||
<div>
|
||||
<h2 className="text-center">
|
||||
Cannot Access Overleaf v1 Project
|
||||
</h2>
|
||||
|
||||
<p className="text-center row-spaced-small">
|
||||
Please contact the project owner or{' '}
|
||||
<a href="/contact">contact support</a> for assistance.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{v1ImportData.status === 'mustLogin' && (
|
||||
<div>
|
||||
<p className="text-center row-spaced-small">
|
||||
You will need to log in to access this project.
|
||||
</p>
|
||||
|
||||
<div className="row-spaced text-center">
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={`/login?redir=${encodeURIComponent(document.location.pathname)}`}
|
||||
>
|
||||
Log in to access project
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{v1ImportData.status === 'canDownloadZip' && (
|
||||
<div>
|
||||
<p className="text-center row-spaced-small">
|
||||
<strong>{v1ImportData.name || 'This project'}</strong> has not
|
||||
yet been moved into the new version of Overleaf. This project
|
||||
was created anonymously and therefore cannot be automatically
|
||||
imported. Please download a zip file of the project and upload
|
||||
that to continue editing it. If you would like to delete this
|
||||
project after you have made a copy, please contact support.
|
||||
</p>
|
||||
|
||||
<div className="row-spaced text-center">
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={`/overleaf/project/${v1ImportData.projectId}/download/zip`}
|
||||
>
|
||||
Download project zip file
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'jquery'
|
||||
import 'bootstrap'
|
||||
import './../utils/meta'
|
||||
import './../utils/webpack-public-path'
|
||||
import './../infrastructure/error-reporter'
|
||||
import './../i18n'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TokenAccessRoot from '../features/token-access/components/token-access-root'
|
||||
|
||||
const element = document.getElementById('token-access-page')
|
||||
if (element) {
|
||||
ReactDOM.render(<TokenAccessRoot />, element)
|
||||
}
|
||||
@@ -5,6 +5,10 @@ export const location = {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
window.location.assign(url)
|
||||
},
|
||||
replace(url) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
window.location.replace(url)
|
||||
},
|
||||
reload() {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
window.location.reload()
|
||||
|
||||
@@ -14,11 +14,20 @@ export const useLocation = () => {
|
||||
[isMounted]
|
||||
)
|
||||
|
||||
const replace = useCallback(
|
||||
url => {
|
||||
if (isMounted.current) {
|
||||
location.replace(url)
|
||||
}
|
||||
},
|
||||
[isMounted]
|
||||
)
|
||||
|
||||
const reload = useCallback(() => {
|
||||
if (isMounted.current) {
|
||||
location.reload()
|
||||
}
|
||||
}, [isMounted])
|
||||
|
||||
return useMemo(() => ({ assign, reload }), [assign, reload])
|
||||
return useMemo(() => ({ assign, replace, reload }), [assign, replace, reload])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user