first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import OLBadge from '@/features/ui/components/ol/ol-badge'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
function trackUpgradeClick() {
|
||||
sendMB('settings-upgrade-click')
|
||||
}
|
||||
|
||||
type EnableWidgetProps = {
|
||||
logo: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
helpPath: string
|
||||
helpTextOverride?: string
|
||||
hasFeature?: boolean
|
||||
isPremiumFeature?: boolean
|
||||
statusIndicator?: ReactNode
|
||||
children?: ReactNode
|
||||
linked?: boolean
|
||||
handleLinkClick: () => void
|
||||
handleUnlinkClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function EnableWidget({
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
helpPath,
|
||||
helpTextOverride,
|
||||
hasFeature,
|
||||
isPremiumFeature,
|
||||
statusIndicator,
|
||||
linked,
|
||||
handleLinkClick,
|
||||
handleUnlinkClick,
|
||||
children,
|
||||
disabled,
|
||||
}: EnableWidgetProps) {
|
||||
const { t } = useTranslation()
|
||||
const helpText = helpTextOverride || t('learn_more')
|
||||
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
<div>{logo}</div>
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4>{title}</h4>
|
||||
{!hasFeature && isPremiumFeature && (
|
||||
<OLBadge bg="info">{t('premium_feature')}</OLBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className="small">
|
||||
{description}{' '}
|
||||
<a href={helpPath} target="_blank" rel="noreferrer">
|
||||
{helpText}
|
||||
</a>
|
||||
</p>
|
||||
{children}
|
||||
{hasFeature && statusIndicator}
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
hasFeature={hasFeature}
|
||||
linked={linked}
|
||||
handleUnlinkClick={handleUnlinkClick}
|
||||
handleLinkClick={handleLinkClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ActionButtonProps = {
|
||||
hasFeature?: boolean
|
||||
linked?: boolean
|
||||
handleUnlinkClick: () => void
|
||||
handleLinkClick: () => void
|
||||
disabled?: boolean
|
||||
linkText?: string
|
||||
unlinkText?: string
|
||||
}
|
||||
|
||||
export function ActionButton({
|
||||
linked,
|
||||
handleUnlinkClick,
|
||||
handleLinkClick,
|
||||
hasFeature,
|
||||
disabled,
|
||||
linkText,
|
||||
unlinkText,
|
||||
}: ActionButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const linkingText = linkText || t('turn_on')
|
||||
const unlinkingText = unlinkText || t('turn_off')
|
||||
if (!hasFeature) {
|
||||
return (
|
||||
<OLButton
|
||||
variant="primary"
|
||||
href="/user/subscription/plans"
|
||||
onClick={trackUpgradeClick}
|
||||
>
|
||||
<span className="text-capitalize">{t('upgrade')}</span>
|
||||
</OLButton>
|
||||
)
|
||||
} else if (linked) {
|
||||
return (
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={handleUnlinkClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{unlinkingText}
|
||||
</OLButton>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{linkingText}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EnableWidget
|
@@ -0,0 +1,218 @@
|
||||
import { useCallback, useState, ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLBadge from '@/features/ui/components/ol/ol-badge'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
|
||||
function trackUpgradeClick(integration: string) {
|
||||
sendMB('settings-upgrade-click', { integration })
|
||||
}
|
||||
|
||||
function trackLinkingClick(integration: string) {
|
||||
sendMB('link-integration-click', { integration, location: 'Settings' })
|
||||
}
|
||||
|
||||
type IntegrationLinkingWidgetProps = {
|
||||
logo: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
helpPath: string
|
||||
hasFeature?: boolean
|
||||
statusIndicator?: ReactNode
|
||||
linked?: boolean
|
||||
linkPath: string
|
||||
unlinkPath: string
|
||||
unlinkConfirmationTitle: string
|
||||
unlinkConfirmationText: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function IntegrationLinkingWidget({
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
helpPath,
|
||||
hasFeature,
|
||||
statusIndicator,
|
||||
linked,
|
||||
linkPath,
|
||||
unlinkPath,
|
||||
unlinkConfirmationTitle,
|
||||
unlinkConfirmationText,
|
||||
disabled,
|
||||
}: IntegrationLinkingWidgetProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const handleUnlinkClick = useCallback(() => {
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
const handleModalHide = useCallback(() => {
|
||||
setShowModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
<div>{logo}</div>
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4>{title}</h4>
|
||||
{!hasFeature && <OLBadge bg="info">{t('premium_feature')}</OLBadge>}
|
||||
</div>
|
||||
<p className="small">
|
||||
{description}{' '}
|
||||
<a href={helpPath} target="_blank" rel="noreferrer">
|
||||
{t('learn_more')}
|
||||
</a>
|
||||
</p>
|
||||
{hasFeature && statusIndicator}
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
integration={title}
|
||||
hasFeature={hasFeature}
|
||||
linked={linked}
|
||||
handleUnlinkClick={handleUnlinkClick}
|
||||
linkPath={linkPath}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<UnlinkConfirmationModal
|
||||
integration={title}
|
||||
show={showModal}
|
||||
title={unlinkConfirmationTitle}
|
||||
content={unlinkConfirmationText}
|
||||
unlinkPath={unlinkPath}
|
||||
handleHide={handleModalHide}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ActionButtonProps = {
|
||||
integration: string
|
||||
hasFeature?: boolean
|
||||
linked?: boolean
|
||||
handleUnlinkClick: () => void
|
||||
linkPath: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
hasFeature,
|
||||
linked,
|
||||
handleUnlinkClick,
|
||||
linkPath,
|
||||
disabled,
|
||||
integration,
|
||||
}: ActionButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
if (!hasFeature) {
|
||||
return (
|
||||
<OLButton
|
||||
variant="primary"
|
||||
href="/user/subscription/plans"
|
||||
onClick={() => trackUpgradeClick(integration)}
|
||||
>
|
||||
<span className="text-capitalize">{t('upgrade')}</span>
|
||||
</OLButton>
|
||||
)
|
||||
} else if (linked) {
|
||||
return (
|
||||
<OLButton
|
||||
variant="danger-ghost"
|
||||
onClick={handleUnlinkClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{disabled ? (
|
||||
<OLButton disabled variant="secondary" className="text-capitalize">
|
||||
{t('link')}
|
||||
</OLButton>
|
||||
) : (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href={linkPath}
|
||||
className="text-capitalize"
|
||||
onClick={() => trackLinkingClick(integration)}
|
||||
>
|
||||
{t('link')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnlinkConfirmModalProps = {
|
||||
show: boolean
|
||||
title: string
|
||||
integration: string
|
||||
content: string
|
||||
unlinkPath: string
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
function UnlinkConfirmationModal({
|
||||
show,
|
||||
title,
|
||||
integration,
|
||||
content,
|
||||
unlinkPath,
|
||||
handleHide,
|
||||
}: UnlinkConfirmModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleCancel = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
handleHide()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
sendMB('unlink-integration-click', {
|
||||
integration,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={handleHide}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{title}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<p>{content}</p>
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<form action={unlinkPath} method="POST" className="form-inline">
|
||||
<input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
|
||||
<OLButton variant="secondary" onClick={handleCancel}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
type="submit"
|
||||
variant="danger-ghost"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
</form>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FetchError } from '../../../../infrastructure/fetch-json'
|
||||
import IEEELogo from '../../../../shared/svgs/ieee-logo'
|
||||
import GoogleLogo from '../../../../shared/svgs/google-logo'
|
||||
import OrcidLogo from '../../../../shared/svgs/orcid-logo'
|
||||
import LinkingStatus from './status'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
|
||||
const providerLogos: { readonly [p: string]: JSX.Element } = {
|
||||
collabratec: <IEEELogo />,
|
||||
google: <GoogleLogo />,
|
||||
orcid: <OrcidLogo />,
|
||||
}
|
||||
|
||||
type SSOLinkingWidgetProps = {
|
||||
providerId: string
|
||||
title: string
|
||||
description: string
|
||||
helpPath?: string
|
||||
linked?: boolean
|
||||
linkPath: string
|
||||
onUnlink: () => Promise<void>
|
||||
}
|
||||
|
||||
export function SSOLinkingWidget({
|
||||
providerId,
|
||||
title,
|
||||
description,
|
||||
helpPath,
|
||||
linked,
|
||||
linkPath,
|
||||
onUnlink,
|
||||
}: SSOLinkingWidgetProps) {
|
||||
const { t } = useTranslation()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [unlinkRequestInflight, setUnlinkRequestInflight] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
const handleUnlinkClick = useCallback(() => {
|
||||
setShowModal(true)
|
||||
setErrorMessage('')
|
||||
}, [])
|
||||
|
||||
const handleUnlinkConfirmationClick = useCallback(() => {
|
||||
setShowModal(false)
|
||||
setUnlinkRequestInflight(true)
|
||||
onUnlink()
|
||||
.catch((error: FetchError) => {
|
||||
setErrorMessage(error.getUserFacingMessage())
|
||||
})
|
||||
.finally(() => {
|
||||
setUnlinkRequestInflight(false)
|
||||
})
|
||||
}, [onUnlink])
|
||||
|
||||
const handleModalHide = useCallback(() => {
|
||||
setShowModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
<div>{providerLogos[providerId]}</div>
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
<p className="small">
|
||||
{description?.replace(/<[^>]+>/g, '')}{' '}
|
||||
{helpPath ? (
|
||||
<a href={helpPath} target="_blank" rel="noreferrer">
|
||||
{t('learn_more')}
|
||||
</a>
|
||||
) : null}
|
||||
</p>
|
||||
{errorMessage ? (
|
||||
<LinkingStatus status="error" description={errorMessage} />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
unlinkRequestInflight={unlinkRequestInflight}
|
||||
accountIsLinked={linked}
|
||||
linkPath={`${linkPath}?intent=link`}
|
||||
onUnlinkClick={handleUnlinkClick}
|
||||
/>
|
||||
</div>
|
||||
<UnlinkConfirmModal
|
||||
title={title}
|
||||
show={showModal}
|
||||
handleConfirmation={handleUnlinkConfirmationClick}
|
||||
handleHide={handleModalHide}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ActionButtonProps = {
|
||||
unlinkRequestInflight: boolean
|
||||
accountIsLinked?: boolean
|
||||
linkPath: string
|
||||
onUnlinkClick: () => void
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
unlinkRequestInflight,
|
||||
accountIsLinked,
|
||||
linkPath,
|
||||
onUnlinkClick,
|
||||
}: ActionButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
if (unlinkRequestInflight) {
|
||||
return (
|
||||
<OLButton variant="danger-ghost" disabled>
|
||||
{t('unlinking')}
|
||||
</OLButton>
|
||||
)
|
||||
} else if (accountIsLinked) {
|
||||
return (
|
||||
<OLButton variant="danger-ghost" onClick={onUnlinkClick}>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<OLButton variant="secondary" href={linkPath} className="text-capitalize">
|
||||
{t('link')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnlinkConfirmModalProps = {
|
||||
title: string
|
||||
show: boolean
|
||||
handleConfirmation: () => void
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
function UnlinkConfirmModal({
|
||||
title,
|
||||
show,
|
||||
handleConfirmation,
|
||||
handleHide,
|
||||
}: UnlinkConfirmModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={handleHide}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{t('unlink_provider_account_title', { provider: title })}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<p>{t('unlink_provider_account_warning', { provider: title })}</p>
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={handleHide}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton variant="danger-ghost" onClick={handleConfirmation}>
|
||||
{t('unlink')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import { ReactNode } from 'react'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
|
||||
type Status = 'pending' | 'success' | 'error'
|
||||
|
||||
type LinkingStatusProps = {
|
||||
status: Status
|
||||
description: string | ReactNode
|
||||
}
|
||||
|
||||
export default function LinkingStatus({
|
||||
status,
|
||||
description,
|
||||
}: LinkingStatusProps) {
|
||||
return (
|
||||
<span>
|
||||
<StatusIcon status={status} />
|
||||
<span className="small"> {description}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type StatusIconProps = {
|
||||
status: Status
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: StatusIconProps) {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return (
|
||||
<Icon
|
||||
type="check-circle"
|
||||
fw
|
||||
className="settings-widget-status-icon status-success"
|
||||
/>
|
||||
)
|
||||
case 'error':
|
||||
return (
|
||||
<Icon
|
||||
type="times-circle"
|
||||
fw
|
||||
className="settings-widget-status-icon status-error"
|
||||
/>
|
||||
)
|
||||
case 'pending':
|
||||
return (
|
||||
<Icon
|
||||
type="circle"
|
||||
fw
|
||||
className="settings-widget-status-icon status-pending"
|
||||
spin
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user