first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { useTranslation, Trans } from 'react-i18next'
import { MergeAndOverride } from '../../../../../../../../types/utils'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import { type UserEmailData } from '../../../../../../../../types/user-email'
type ConfirmationModalProps = MergeAndOverride<
React.ComponentProps<typeof OLModal>,
{
email: string
isConfirmDisabled: boolean
onConfirm: () => void
onHide: () => void
primary?: UserEmailData
}
>
function ConfirmationModal({
email,
isConfirmDisabled,
show,
onConfirm,
onHide,
primary,
}: ConfirmationModalProps) {
const { t } = useTranslation()
return (
<OLModal show={show} onHide={onHide}>
<OLModalHeader closeButton>
<OLModalTitle>{t('confirm_primary_email_change')}</OLModalTitle>
</OLModalHeader>
<OLModalBody className="pb-0">
<p>
<Trans
i18nKey="do_you_want_to_change_your_primary_email_address_to"
components={{ b: <b /> }}
values={{ email }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
<p>{t('log_in_with_primary_email_address')}</p>
{primary && !primary.confirmedAt && (
<p>
<Trans
i18nKey="this_will_remove_primary_email"
components={{ b: <b /> }}
values={{ email: primary.email }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
)}
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={onHide}>
{t('cancel')}
</OLButton>
<OLButton
variant="primary"
disabled={isConfirmDisabled}
onClick={onConfirm}
>
{t('change_primary_email')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
export default ConfirmationModal

View File

@@ -0,0 +1,132 @@
import { useState } from 'react'
import PrimaryButton from './primary-button'
import { useTranslation } from 'react-i18next'
import {
inReconfirmNotificationPeriod,
institutionAlreadyLinked,
} from '../../../../utils/selectors'
import { postJSON } from '../../../../../../infrastructure/fetch-json'
import {
State,
useUserEmailsContext,
} from '../../../../context/user-email-context'
import { UserEmailData } from '../../../../../../../../types/user-email'
import { UseAsyncReturnType } from '../../../../../../shared/hooks/use-async'
import { ssoAvailableForInstitution } from '../../../../utils/sso'
import ConfirmationModal from './confirmation-modal'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
const getDescription = (
t: (s: string) => string,
state: State,
userEmailData: UserEmailData
) => {
if (inReconfirmNotificationPeriod(userEmailData)) {
return t('please_reconfirm_your_affiliation_before_making_this_primary')
}
if (userEmailData.confirmedAt) {
return t('make_email_primary_description')
}
const ssoAvailable = ssoAvailableForInstitution(
userEmailData.affiliation?.institution || null
)
if (!institutionAlreadyLinked(state, userEmailData) && ssoAvailable) {
return t('please_link_before_making_primary')
}
return t('please_confirm_your_email_before_making_it_default')
}
type MakePrimaryProps = {
userEmailData: UserEmailData
primary?: UserEmailData
makePrimaryAsync: UseAsyncReturnType
}
function MakePrimary({
userEmailData,
primary,
makePrimaryAsync,
}: MakePrimaryProps) {
const [show, setShow] = useState(false)
const { t } = useTranslation()
const { state, makePrimary, deleteEmail, resetLeaversSurveyExpiration } =
useUserEmailsContext()
const handleShowModal = () => setShow(true)
const handleHideModal = () => setShow(false)
const handleSetDefaultUserEmail = () => {
handleHideModal()
makePrimaryAsync
.runAsync(
// 'delete-unconfirmed-primary' is a temporary parameter here to keep backward compatibility.
// So users with the old version of the frontend don't get their primary email deleted unexpectedly.
// https://github.com/overleaf/internal/issues/23536
postJSON('/user/emails/default?delete-unconfirmed-primary', {
body: {
email: userEmailData.email,
},
})
)
.then(() => {
makePrimary(userEmailData.email)
if (primary && !primary.confirmedAt) {
deleteEmail(primary.email)
resetLeaversSurveyExpiration(primary)
}
})
.catch(() => {})
}
if (userEmailData.default) {
return null
}
const isConfirmDisabled = Boolean(
!userEmailData.confirmedAt ||
state.isLoading ||
inReconfirmNotificationPeriod(userEmailData)
)
return (
<>
{makePrimaryAsync.isLoading ? (
<PrimaryButton disabled isLoading={state.isLoading}>
{t('processing_uppercase')}&hellip;
</PrimaryButton>
) : (
<OLTooltip
id={`make-primary-${userEmailData.email}`}
description={getDescription(t, state, userEmailData)}
>
{/*
Disabled buttons don't work with tooltips, due to pointer-events: none,
so create a wrapper for the tooltip
*/}
<span>
<PrimaryButton
disabled={isConfirmDisabled}
onClick={handleShowModal}
>
{t('make_primary')}
</PrimaryButton>
</span>
</OLTooltip>
)}
<ConfirmationModal
email={userEmailData.email}
isConfirmDisabled={isConfirmDisabled}
primary={primary}
show={show}
onHide={handleHideModal}
onConfirm={handleSetDefaultUserEmail}
/>
</>
)
}
export default MakePrimary

View File

@@ -0,0 +1,22 @@
import OLButton, { OLButtonProps } from '@/features/ui/components/ol/ol-button'
function PrimaryButton({
children,
disabled,
isLoading,
onClick,
}: OLButtonProps) {
return (
<OLButton
size="sm"
disabled={disabled && !isLoading}
isLoading={isLoading}
onClick={onClick}
variant="secondary"
>
{children}
</OLButton>
)
}
export default PrimaryButton

View File

@@ -0,0 +1,89 @@
import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../../types/user-email'
import { useUserEmailsContext } from '../../../context/user-email-context'
import { postJSON } from '../../../../../infrastructure/fetch-json'
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton, {
OLIconButtonProps,
} from '@/features/ui/components/ol/ol-icon-button'
import getMeta from '@/utils/meta'
type DeleteButtonProps = Pick<
OLIconButtonProps,
'disabled' | 'isLoading' | 'onClick'
>
function DeleteButton({ disabled, isLoading, onClick }: DeleteButtonProps) {
const { t } = useTranslation()
return (
<OLIconButton
variant="danger"
disabled={disabled}
isLoading={isLoading}
size="sm"
onClick={onClick}
accessibilityLabel={t('remove') || ''}
icon="delete"
/>
)
}
type RemoveProps = {
userEmailData: UserEmailData
deleteEmailAsync: UseAsyncReturnType
}
function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
const { t } = useTranslation()
const { state, deleteEmail, resetLeaversSurveyExpiration } =
useUserEmailsContext()
const isManaged = getMeta('ol-isManagedAccount')
const getTooltipText = () => {
if (isManaged) {
return t('your_account_is_managed_by_your_group_admin')
}
return userEmailData.default
? t('please_change_primary_to_remove')
: t('remove')
}
const handleRemoveUserEmail = () => {
deleteEmailAsync
.runAsync(
postJSON('/user/emails/delete', {
body: {
email: userEmailData.email,
},
})
)
.then(() => {
deleteEmail(userEmailData.email)
resetLeaversSurveyExpiration(userEmailData)
})
.catch(() => {})
}
if (deleteEmailAsync.isLoading) {
return <DeleteButton isLoading />
}
return (
<OLTooltip
id={userEmailData.email}
description={getTooltipText()}
overlayProps={{ placement: userEmailData.default ? 'left' : 'top' }}
>
<span>
<DeleteButton
disabled={state.isLoading || userEmailData.default}
onClick={handleRemoveUserEmail}
/>
</span>
</OLTooltip>
)
}
export default Remove