first commit
This commit is contained in:
@@ -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,53 @@
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type LeaveProjectModalProps = {
|
||||
showModal: boolean
|
||||
handleCloseModal: () => void
|
||||
handleLeaveAction: () => void
|
||||
}
|
||||
function LeaveProjectModal({
|
||||
showModal,
|
||||
handleCloseModal,
|
||||
handleLeaveAction,
|
||||
}: LeaveProjectModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLModal
|
||||
animation
|
||||
show={showModal}
|
||||
onHide={handleCloseModal}
|
||||
id="action-project-modal"
|
||||
backdrop="static"
|
||||
>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('leave_project')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
<p>{t('about_to_leave_project')}</p>
|
||||
<Notification
|
||||
content={t('this_action_cannot_be_undone')}
|
||||
type="warning"
|
||||
/>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={handleCloseModal}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton variant="danger" onClick={() => handleLeaveAction()}>
|
||||
{t('confirm')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveProjectModal
|
||||
@@ -0,0 +1,69 @@
|
||||
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="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="text-center link-sharing-invite">
|
||||
<div className="link-sharing-invite-header">
|
||||
<p>
|
||||
{t('youre_joining')}
|
||||
<br />
|
||||
<em>
|
||||
<strong>
|
||||
{requireAcceptData.projectName || 'This project'}
|
||||
</strong>
|
||||
</em>
|
||||
{user && (
|
||||
<>
|
||||
<br />
|
||||
{t('as_email', { email: user.email })}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row-spaced text-center">
|
||||
<div className="col-md-12">
|
||||
<p>
|
||||
{t(
|
||||
'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row-spaced text-center">
|
||||
<div className="col-md-12">
|
||||
<button
|
||||
className="btn btn-lg btn-primary"
|
||||
type="submit"
|
||||
onClick={() => sendPostRequest(true)}
|
||||
>
|
||||
{t('ok_join_project')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
|
||||
import { useCallback, useState } from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
import LeaveProjectModal from './leave-project-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
function SharingUpdatesRoot() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { isReady } = useWaitForI18n()
|
||||
const { t } = useTranslation()
|
||||
const { isLoading, isSuccess, isError, runAsync } = useAsync()
|
||||
const projectId = getMeta('ol-project_id')
|
||||
|
||||
const joinProject = useCallback(() => {
|
||||
sendMB('notification-click', {
|
||||
name: 'link-sharing-collaborator',
|
||||
button: 'ok',
|
||||
})
|
||||
runAsync(postJSON(`/project/${projectId}/sharing-updates/join`))
|
||||
.then(() => {
|
||||
location.assign(`/project/${projectId}`)
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
}, [runAsync, projectId])
|
||||
|
||||
const viewProject = useCallback(() => {
|
||||
sendMB('notification-click', {
|
||||
name: 'link-sharing-collaborator',
|
||||
button: 'anonymous',
|
||||
})
|
||||
runAsync(postJSON(`/project/${projectId}/sharing-updates/view`))
|
||||
.then(() => {
|
||||
location.assign(`/project/${projectId}`)
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
}, [runAsync, projectId])
|
||||
|
||||
const leaveProject = useCallback(() => {
|
||||
sendMB('notification-click', {
|
||||
name: 'link-sharing-collaborator',
|
||||
button: 'leave',
|
||||
})
|
||||
runAsync(postJSON(`/project/${projectId}/leave`))
|
||||
.then(() => {
|
||||
location.assign('/project')
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
}, [runAsync, projectId])
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<LeaveProjectModal
|
||||
handleLeaveAction={leaveProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={() => setShowModal(false)}
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-md-6 col-md-offset-3 offset-md-3">
|
||||
<div className="card sharing-updates">
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<h1 className="sharing-updates-h1">
|
||||
{t('updates_to_project_sharing')}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row-spaced">
|
||||
<div className="col-md-12">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="were_making_some_changes_to_project_sharing_this_means_you_will_be_visible"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a
|
||||
href="/blog/changes-to-project-sharing"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
sendMB('notification-click', {
|
||||
name: 'link-sharing-collaborator',
|
||||
button: 'learn',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row-spaced">
|
||||
<div className="col-md-12">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={joinProject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('ok_continue_to_project')}
|
||||
</OLButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isError && (
|
||||
<div className="row row-spaced">
|
||||
<div className="col-md-12">
|
||||
<Notification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row row-spaced">
|
||||
<div className="col-md-12">
|
||||
<p>
|
||||
<small>
|
||||
<Trans
|
||||
i18nKey="you_can_also_choose_to_view_anonymously_or_leave_the_project"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
className="btn btn-inline-link"
|
||||
onClick={() => viewProject()}
|
||||
disabled={isLoading || isSuccess}
|
||||
/>,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
className="btn btn-inline-link"
|
||||
onClick={() => setShowModal(true)}
|
||||
disabled={isLoading || isSuccess}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(
|
||||
SharingUpdatesRoot,
|
||||
GenericErrorBoundaryFallback
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
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 { ElementType, 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 { 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'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
|
||||
type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
|
||||
|
||||
const [v1ImportDataScreenModule] = importOverleafModules(
|
||||
'v1ImportDataScreen'
|
||||
) as {
|
||||
import: { default: ElementType }
|
||||
}[]
|
||||
const V1ImportDataScreen = v1ImportDataScreenModule?.import.default
|
||||
|
||||
export type V1ImportData = {
|
||||
name?: string
|
||||
status: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// We don't want the full-size div and back link(?) on
|
||||
// the new page, but we do this so the original page
|
||||
// doesn't change.
|
||||
// TODO: clean up the DOM in the main return
|
||||
if (mode === 'requireAccept' && requireAcceptData) {
|
||||
return (
|
||||
<RequireAcceptScreen
|
||||
requireAcceptData={requireAcceptData}
|
||||
sendPostRequest={sendPostRequest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{V1ImportDataScreen && mode === 'v1Import' && v1ImportData && (
|
||||
<V1ImportDataScreen v1ImportData={v1ImportData} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)
|
||||
Reference in New Issue
Block a user