Merge pull request #17990 from overleaf/rd-button-links

[web] Migrating buttons to Bootstrap 5 on the Account Settings page

GitOrigin-RevId: c9dfa9b1dee50f4c0b30abf8ac464e53cf98db95
This commit is contained in:
Rebeka Dekany 2024-04-23 16:12:25 +02:00 committed by Copybot
parent 06f34c71bc
commit 898acab307
25 changed files with 277 additions and 146 deletions

View file

@ -124,7 +124,8 @@ function AccountInfoSection() {
type="submit" type="submit"
variant="primary" variant="primary"
form="account-info-form" form="account-info-form"
isLoading={isLoading || !isFormValid} disabled={!isFormValid}
isLoading={isLoading}
bs3Props={{ bs3Props={{
loading: isLoading ? `${t('saving')}` : t('update'), loading: isLoading ? `${t('saving')}` : t('update'),
}} }}

View file

@ -1,7 +1,8 @@
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { Modal, Button } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import AccessibleModal from '../../../../../../shared/components/accessible-modal' import AccessibleModal from '../../../../../../shared/components/accessible-modal'
import { MergeAndOverride } from '../../../../../../../../types/utils' import { MergeAndOverride } from '../../../../../../../../types/utils'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type ConfirmationModalProps = MergeAndOverride< type ConfirmationModalProps = MergeAndOverride<
React.ComponentProps<typeof AccessibleModal>, React.ComponentProps<typeof AccessibleModal>,
@ -40,22 +41,24 @@ function ConfirmationModal({
<p className="mb-0">{t('log_in_with_primary_email_address')}</p> <p className="mb-0">{t('log_in_with_primary_email_address')}</p>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button <ButtonWrapper
bsStyle={null} variant="secondary"
className="btn-secondary-info btn-secondary"
onClick={onHide} onClick={onHide}
bs3Props={{
bsStyle: null,
className: 'btn-secondary-info btn-secondary',
}}
> >
{t('cancel')} {t('cancel')}
</Button> </ButtonWrapper>
<Button <ButtonWrapper
type="button" variant="primary"
bsStyle={null}
className="btn-primary"
disabled={isConfirmDisabled} disabled={isConfirmDisabled}
onClick={onConfirm} onClick={onConfirm}
bs3Props={{ bsStyle: null, className: 'btn-primary' }}
> >
{t('confirm')} {t('confirm')}
</Button> </ButtonWrapper>
</Modal.Footer> </Modal.Footer>
</AccessibleModal> </AccessibleModal>
) )

View file

@ -82,7 +82,7 @@ function MakePrimary({ userEmailData, makePrimaryAsync }: MakePrimaryProps) {
return ( return (
<> <>
{makePrimaryAsync.isLoading ? ( {makePrimaryAsync.isLoading ? (
<PrimaryButton disabled> <PrimaryButton disabled isLoading={state.isLoading}>
{t('processing_uppercase')}&hellip; {t('processing_uppercase')}&hellip;
</PrimaryButton> </PrimaryButton>
) : ( ) : (

View file

@ -1,19 +1,24 @@
import ButtonWrapper, { import ButtonWrapper, {
ButtonWrapperProps, ButtonWrapperProps,
} from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper' } from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5'
function PrimaryButton({ children, disabled, onClick }: ButtonWrapperProps) { function PrimaryButton({
children,
disabled,
isLoading,
onClick,
}: ButtonWrapperProps) {
return ( return (
<ButtonWrapper <ButtonWrapper
size="small" size="small"
disabled={disabled} disabled={disabled && !isLoading}
isLoading={isLoading}
onClick={onClick} onClick={onClick}
variant="secondary" variant="secondary"
bs3Props={{ bsStyle: null }} bs3Props={{
className={bsVersion({ bsStyle: null,
bs3: 'btn-secondary btn-secondary-info', className: 'btn-secondary btn-secondary-info',
})} }}
> >
{children} {children}
</ButtonWrapper> </ButtonWrapper>

View file

@ -214,7 +214,8 @@ function AddEmail() {
> >
<AddNewEmailBtn <AddNewEmailBtn
email={newEmail} email={newEmail}
disabled={isLoading || state.isLoading} disabled={state.isLoading}
isLoading={isLoading}
onClick={handleAddNewEmail} onClick={handleAddNewEmail}
/> />
</Cell> </Cell>

View file

@ -1,18 +1,20 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap' import ButtonWrapper, {
ButtonWrapperProps,
} from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
function AddAnotherEmailBtn({ onClick, ...props }: ButtonProps) { function AddAnotherEmailBtn({ onClick, ...props }: ButtonWrapperProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Button <ButtonWrapper
className="btn-inline-link" variant="link"
onClick={onClick} onClick={onClick}
{...props} {...props}
bsStyle={null} bs3Props={{ bsStyle: null, className: 'btn-inline-link' }}
> >
{t('add_another_email')} {t('add_another_email')}
</Button> </ButtonWrapper>
) )
} }

View file

@ -1,5 +1,7 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap' import ButtonWrapper, {
ButtonWrapperProps,
} from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
const isValidEmail = (email: string) => { const isValidEmail = (email: string) => {
return Boolean(email) return Boolean(email)
@ -7,20 +9,26 @@ const isValidEmail = (email: string) => {
type AddNewEmailColProps = { type AddNewEmailColProps = {
email: string email: string
} & ButtonProps } & ButtonWrapperProps
function AddNewEmailBtn({ email, disabled, ...props }: AddNewEmailColProps) { function AddNewEmailBtn({
email,
disabled,
isLoading,
...props
}: AddNewEmailColProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Button <ButtonWrapper
bsSize="small" size="small"
bsStyle="primary" variant="primary"
disabled={disabled || !isValidEmail(email)} disabled={(disabled && !isLoading) || !isValidEmail(email)}
isLoading={isLoading}
{...props} {...props}
> >
{t('add_new_email')} {t('add_new_email')}
</Button> </ButtonWrapper>
) )
} }

View file

@ -1,20 +1,25 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap' import ButtonWrapper, {
ButtonWrapperProps,
} from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
function EmailAffiliatedWithInstitution({ onClick, ...props }: ButtonProps) { function EmailAffiliatedWithInstitution({
onClick,
...props
}: ButtonWrapperProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="mt-1"> <div className="mt-1">
{t('is_email_affiliated')} {t('is_email_affiliated')}
<Button <ButtonWrapper
className="btn-inline-link" variant="link"
onClick={onClick} onClick={onClick}
bs3Props={{ bsStyle: null, className: 'btn-inline-link' }}
{...props} {...props}
bsStyle={null}
> >
{t('let_us_know')} {t('let_us_know')}
</Button> </ButtonWrapper>
</div> </div>
) )
} }

View file

@ -1,10 +1,10 @@
import { useState } from 'react' import { useState } from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { DomainInfo } from './input' import { DomainInfo } from './input'
import { ExposedSettings } from '../../../../../../../types/exposed-settings' import { ExposedSettings } from '../../../../../../../types/exposed-settings'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
import { useLocation } from '../../../../../shared/hooks/use-location' import { useLocation } from '../../../../../shared/hooks/use-location'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type SSOLinkingInfoProps = { type SSOLinkingInfoProps = {
domainInfo: DomainInfo domainInfo: DomainInfo
@ -54,14 +54,15 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
{t('find_out_more_about_institution_login')}. {t('find_out_more_about_institution_login')}.
</a> </a>
</p> </p>
<Button <ButtonWrapper
bsStyle="primary" variant="primary"
className="btn-sm btn-link-accounts" className="btn-link-accounts"
size="small"
disabled={linkAccountsButtonDisabled} disabled={linkAccountsButtonDisabled}
onClick={handleLinkAccountsButtonClick} onClick={handleLinkAccountsButtonClick}
> >
{t('link_accounts_and_add_email')} {t('link_accounts_and_add_email')}
</Button> </ButtonWrapper>
</> </>
) )
} }

View file

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type UniversityNameProps = { type UniversityNameProps = {
name: string name: string
@ -14,9 +14,13 @@ function UniversityName({ name, onClick }: UniversityNameProps) {
{name} {name}
<span className="small"> <span className="small">
{' '} {' '}
<Button className="btn-inline-link" onClick={onClick} bsStyle={null}> <ButtonWrapper
variant="link"
onClick={onClick}
bs3Props={{ bsStyle: null, className: 'btn-inline-link' }}
>
{t('change')} {t('change')}
</Button> </ButtonWrapper>
</span> </span>
</p> </p>
) )

View file

@ -1,7 +1,6 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email' import { UserEmailData } from '../../../../../../types/user-email'
import { Button } from 'react-bootstrap'
import { isChangingAffiliation } from '../../utils/selectors' import { isChangingAffiliation } from '../../utils/selectors'
import { useUserEmailsContext } from '../../context/user-email-context' import { useUserEmailsContext } from '../../context/user-email-context'
import DownshiftInput from './downshift-input' import DownshiftInput from './downshift-input'
@ -10,6 +9,7 @@ import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
import defaultRoles from '../../data/roles' import defaultRoles from '../../data/roles'
import defaultDepartments from '../../data/departments' import defaultDepartments from '../../data/departments'
import { University } from '../../../../../../types/university' import { University } from '../../../../../../types/university'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type InstitutionAndRoleProps = { type InstitutionAndRoleProps = {
userEmailData: UserEmailData userEmailData: UserEmailData
@ -107,11 +107,15 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
<br /> <br />
</> </>
)} )}
<Button className="btn-inline-link" onClick={handleChangeAffiliation}> <ButtonWrapper
onClick={handleChangeAffiliation}
variant="link"
bs3Props={{ className: 'btn-inline-link' }}
>
{!affiliation.department && !affiliation.role {!affiliation.department && !affiliation.role
? t('add_role_and_department') ? t('add_role_and_department')
: t('change')} : t('change')}
</Button> </ButtonWrapper>
</div> </div>
) : ( ) : (
<div className="affiliation-change-container small"> <div className="affiliation-change-container small">
@ -135,23 +139,30 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
setValue={setDepartment} setValue={setDepartment}
/> />
</div> </div>
<Button <ButtonWrapper
bsSize="small" size="small"
bsStyle="primary" variant="primary"
type="submit" type="submit"
disabled={!role || !department || isLoading || state.isLoading} disabled={!role || !department}
isLoading={isLoading}
bs3Props={{
loading: isLoading
? `${t('saving')}`
: t('save_or_cancel-save'),
}}
> >
{isLoading ? <>{t('saving')}</> : t('save_or_cancel-save')} {t('save_or_cancel-save')}
</Button> </ButtonWrapper>
{!isLoading && ( {!isLoading && (
<> <>
<span className="mx-1">{t('save_or_cancel-or')}</span> <span className="mx-1">{t('save_or_cancel-or')}</span>
<Button <ButtonWrapper
className="btn-inline-link" variant="link"
onClick={handleCancelAffiliationChange} onClick={handleCancelAffiliationChange}
bs3Props={{ className: 'btn-inline-link' }}
> >
{t('save_or_cancel-cancel')} {t('save_or_cancel-cancel')}
</Button> </ButtonWrapper>
</> </>
)} )}
</form> </form>

View file

@ -146,9 +146,13 @@ function ReconfirmationInfo({ userEmailData }: ReconfirmationInfoProps) {
</> </>
) : ( ) : (
<ButtonWrapper <ButtonWrapper
className="btn-inline-link" variant="link"
disabled={state.isLoading} disabled={state.isLoading}
onClick={handleRequestReconfirmation} onClick={handleRequestReconfirmation}
bs3Props={{
className: 'btn-inline-link',
bsStyle: null,
}}
> >
{t('resend_confirmation_email')} {t('resend_confirmation_email')}
</ButtonWrapper> </ButtonWrapper>
@ -157,7 +161,8 @@ function ReconfirmationInfo({ userEmailData }: ReconfirmationInfoProps) {
) : ( ) : (
<ButtonWrapper <ButtonWrapper
variant="secondary" variant="secondary"
disabled={state.isLoading || isPending} disabled={isPending}
isLoading={isLoading}
onClick={handleRequestReconfirmation} onClick={handleRequestReconfirmation}
bs3Props={{ bsStyle: 'info' }} bs3Props={{ bsStyle: 'info' }}
> >
@ -200,9 +205,10 @@ function ReconfirmationInfo({ userEmailData }: ReconfirmationInfoProps) {
</> </>
) : ( ) : (
<ButtonWrapper <ButtonWrapper
className="btn-inline-link" variant="link"
disabled={state.isLoading} disabled={state.isLoading}
onClick={handleRequestReconfirmation} onClick={handleRequestReconfirmation}
bs3Props={{ className: 'btn-inline-link', bsStyle: null }}
> >
{t('resend_confirmation_email')} {t('resend_confirmation_email')}
</ButtonWrapper> </ButtonWrapper>

View file

@ -1,11 +1,11 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import { Button } from 'react-bootstrap'
import { FetchError, postJSON } from '../../../../infrastructure/fetch-json' import { FetchError, postJSON } from '../../../../infrastructure/fetch-json'
import useAsync from '../../../../shared/hooks/use-async' import useAsync from '../../../../shared/hooks/use-async'
import { UserEmailData } from '../../../../../../types/user-email' import { UserEmailData } from '../../../../../../types/user-email'
import { useUserEmailsContext } from '../../context/user-email-context' import { useUserEmailsContext } from '../../context/user-email-context'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type ResendConfirmationEmailButtonProps = { type ResendConfirmationEmailButtonProps = {
email: UserEmailData['email'] email: UserEmailData['email']
@ -47,14 +47,14 @@ function ResendConfirmationEmailButton({
return ( return (
<> <>
<Button <ButtonWrapper
className="btn-inline-link" variant="link"
disabled={state.isLoading} disabled={state.isLoading || isLoading}
onClick={handleResendConfirmationEmail} onClick={handleResendConfirmationEmail}
bsStyle={null} bs3Props={{ bsStyle: null, className: 'btn-inline-link' }}
> >
{t('resend_confirmation_email')} {t('resend_confirmation_email')}
</Button> </ButtonWrapper>
<br /> <br />
{isError && ( {isError && (
<div className="text-danger"> <div className="text-danger">

View file

@ -1,7 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email' import { UserEmailData } from '../../../../../../types/user-email'
import { Button } from 'react-bootstrap'
import Email from './email' import Email from './email'
import InstitutionAndRole from './institution-and-role' import InstitutionAndRole from './institution-and-role'
import EmailCell from './cell' import EmailCell from './cell'
@ -16,6 +15,7 @@ import { useLocation } from '../../../../shared/hooks/use-location'
import RowWrapper from '@/features/ui/components/bootstrap-5/wrappers/row-wrapper' import RowWrapper from '@/features/ui/components/bootstrap-5/wrappers/row-wrapper'
import ColWrapper from '@/features/ui/components/bootstrap-5/wrappers/col-wrapper' import ColWrapper from '@/features/ui/components/bootstrap-5/wrappers/col-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5' import { bsVersion } from '@/features/utils/bootstrap-5'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type EmailsRowProps = { type EmailsRowProps = {
userEmailData: UserEmailData userEmailData: UserEmailData
@ -160,14 +160,15 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
})} })}
> >
<EmailCell> <EmailCell>
<Button <ButtonWrapper
bsStyle="primary" variant="primary"
className="btn-sm btn-link-accounts" className="btn-link-accounts"
disabled={linkAccountsButtonDisabled} disabled={linkAccountsButtonDisabled}
onClick={handleLinkAccountsButtonClick} onClick={handleLinkAccountsButtonClick}
size="small"
> >
{t('link_accounts')} {t('link_accounts')}
</Button> </ButtonWrapper>
</EmailCell> </EmailCell>
</ColWrapper> </ColWrapper>
</RowWrapper> </RowWrapper>

View file

@ -2,6 +2,8 @@ import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LeaveModal from './leave/modal' import LeaveModal from './leave/modal'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5'
function LeaveSection() { function LeaveSection() {
const { t } = useTranslation() const { t } = useTranslation()
@ -28,9 +30,16 @@ function LeaveSection() {
return ( return (
<> <>
{t('need_to_leave')}{' '} {t('need_to_leave')}{' '}
<button className="btn btn-inline-link btn-danger" onClick={handleOpen}> <ButtonWrapper
className={bsVersion({
bs3: 'btn btn-inline-link btn-danger',
bs5: 'btn-link',
})}
variant="danger"
onClick={handleOpen}
>
{t('delete_your_account')} {t('delete_your_account')}
</button> </ButtonWrapper>
<LeaveModal isOpen={isModalOpen} handleClose={handleClose} /> <LeaveModal isOpen={isModalOpen} handleClose={handleClose} />
</> </>
) )

View file

@ -1,9 +1,10 @@
import { useState, Dispatch, SetStateAction } from 'react' import { useState, Dispatch, SetStateAction } from 'react'
import { Modal, Button } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import getMeta from '../../../../utils/meta' import getMeta from '../../../../utils/meta'
import LeaveModalForm, { LeaveModalFormProps } from './modal-form' import LeaveModalForm, { LeaveModalFormProps } from './modal-form'
import { ExposedSettings } from '../../../../../../types/exposed-settings' import { ExposedSettings } from '../../../../../../types/exposed-settings'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
type LeaveModalContentProps = { type LeaveModalContentProps = {
handleHide: () => void handleHide: () => void
@ -68,24 +69,23 @@ function LeaveModalContent({
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button <ButtonWrapper
type="button"
disabled={inFlight} disabled={inFlight}
onClick={handleHide} onClick={handleHide}
bsStyle={null} variant="secondary"
className="btn-secondary" bs3Props={{ bsStyle: null, className: 'btn-secondary' }}
> >
{t('cancel')} {t('cancel')}
</Button> </ButtonWrapper>
<Button <ButtonWrapper
form="leave-form" form="leave-form"
type="submit" type="submit"
bsStyle="danger" variant="danger"
disabled={inFlight || !isFormValid} disabled={inFlight || !isFormValid}
> >
{inFlight ? <>{t('deleting')}</> : t('delete')} {inFlight ? <>{t('deleting')}</> : t('delete')}
</Button> </ButtonWrapper>
</Modal.Footer> </Modal.Footer>
</> </>
) )

View file

@ -1,8 +1,8 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper' import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
function trackUpgradeClick() { function trackUpgradeClick() {
sendMB('settings-upgrade-click') sendMB('settings-upgrade-click')
@ -92,36 +92,39 @@ function ActionButton({
const { t } = useTranslation() const { t } = useTranslation()
if (!hasFeature) { if (!hasFeature) {
return ( return (
<Button <ButtonWrapper
bsStyle={null} variant="primary"
className="btn-primary"
href="/user/subscription/plans" href="/user/subscription/plans"
onClick={trackUpgradeClick} onClick={trackUpgradeClick}
bs3Props={{ bsStyle: null, className: 'btn-primary' }}
> >
<span className="text-capitalize">{t('upgrade')}</span> <span className="text-capitalize">{t('upgrade')}</span>
</Button> </ButtonWrapper>
) )
} else if (linked) { } else if (linked) {
return ( return (
<Button <ButtonWrapper
className="btn-danger-ghost" variant="danger-ghost"
onClick={handleUnlinkClick} onClick={handleUnlinkClick}
bsStyle={null}
disabled={disabled} disabled={disabled}
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
> >
{t('turn_off')} {t('turn_off')}
</Button> </ButtonWrapper>
) )
} else { } else {
return ( return (
<Button <ButtonWrapper
variant="secondary"
disabled={disabled} disabled={disabled}
bsStyle={null}
onClick={handleLinkClick} onClick={handleLinkClick}
className="btn btn-secondary-info btn-secondary text-capitalize" bs3Props={{
bsStyle: null,
className: 'btn btn-secondary-info btn-secondary',
}}
> >
{t('turn_on')} {t('turn_on')}
</Button> </ButtonWrapper>
) )
} }
} }

View file

@ -1,10 +1,12 @@
import { useCallback, useState, ReactNode } from 'react' import { useCallback, useState, ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AccessibleModal from '../../../../shared/components/accessible-modal' import AccessibleModal from '../../../../shared/components/accessible-modal'
import { Modal } from 'react-bootstrap'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper' import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
import { Button, Modal } from 'react-bootstrap'
import getMeta from '../../../../utils/meta' import getMeta from '../../../../utils/meta'
import { sendMB } from '../../../../infrastructure/event-tracking' import { sendMB } from '../../../../infrastructure/event-tracking'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5'
function trackUpgradeClick() { function trackUpgradeClick() {
sendMB('settings-upgrade-click') sendMB('settings-upgrade-click')
@ -107,43 +109,52 @@ function ActionButton({
const { t } = useTranslation() const { t } = useTranslation()
if (!hasFeature) { if (!hasFeature) {
return ( return (
<Button <ButtonWrapper
bsStyle={null} variant="primary"
className="btn-primary"
href="/user/subscription/plans" href="/user/subscription/plans"
onClick={trackUpgradeClick} onClick={trackUpgradeClick}
bs3Props={{ bsStyle: null, className: 'btn-primary' }}
> >
<span className="text-capitalize">{t('upgrade')}</span> <span className="text-capitalize">{t('upgrade')}</span>
</Button> </ButtonWrapper>
) )
} else if (linked) { } else if (linked) {
return ( return (
<Button <ButtonWrapper
className="btn-danger-ghost" variant="danger-ghost"
onClick={handleUnlinkClick} onClick={handleUnlinkClick}
bsStyle={null}
disabled={disabled} disabled={disabled}
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
> >
{t('unlink')} {t('unlink')}
</Button> </ButtonWrapper>
) )
} else { } else {
return ( return (
<> <>
{disabled ? ( {disabled ? (
<button <ButtonWrapper
disabled disabled
className="btn btn-secondary-info btn-secondary text-capitalize" variant="secondary"
className={bsVersion({
bs3: 'btn btn-secondary-info btn-secondary text-capitalize',
bs5: 'text-capitalize',
})}
> >
{t('link')} {t('link')}
</button> </ButtonWrapper>
) : ( ) : (
<a <ButtonWrapper
className="btn btn-secondary-info btn-secondary text-capitalize" variant="secondary"
href={linkPath} href={linkPath}
className={bsVersion({
bs3: 'btn btn-secondary-info btn-secondary text-capitalize',
bs5: 'text-capitalize',
})}
bs3Props={{ bsStyle: null }}
> >
{t('link')} {t('link')}
</a> </ButtonWrapper>
)} )}
</> </>
) )
@ -167,9 +178,7 @@ function UnlinkConfirmationModal({
}: UnlinkConfirmModalProps) { }: UnlinkConfirmModalProps) {
const { t } = useTranslation() const { t } = useTranslation()
const handleCancel = ( const handleCancel = (event: React.MouseEvent<HTMLButtonElement>) => {
event: React.MouseEvent<HTMLButtonElement & Button>
) => {
event.preventDefault() event.preventDefault()
handleHide() handleHide()
} }
@ -186,15 +195,23 @@ function UnlinkConfirmationModal({
<Modal.Footer> <Modal.Footer>
<form action={unlinkPath} method="POST" className="form-inline"> <form action={unlinkPath} method="POST" className="form-inline">
<input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} /> <input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
<Button <ButtonWrapper
className="btn-secondary-info btn-secondary" variant="secondary"
onClick={handleCancel} onClick={handleCancel}
bs3Props={{
bsStyle: null,
className: 'btn-secondary-info btn-secondary',
}}
> >
{t('cancel')} {t('cancel')}
</Button> </ButtonWrapper>
<Button type="submit" className="btn-danger-ghost" bsStyle={null}> <ButtonWrapper
type="submit"
variant="danger-ghost"
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
>
{t('unlink')} {t('unlink')}
</Button> </ButtonWrapper>
</form> </form>
</Modal.Footer> </Modal.Footer>
</AccessibleModal> </AccessibleModal>

View file

@ -1,12 +1,14 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Modal } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import { FetchError } from '../../../../infrastructure/fetch-json' import { FetchError } from '../../../../infrastructure/fetch-json'
import AccessibleModal from '../../../../shared/components/accessible-modal' import AccessibleModal from '../../../../shared/components/accessible-modal'
import IEEELogo from '../../../../shared/svgs/ieee-logo' import IEEELogo from '../../../../shared/svgs/ieee-logo'
import GoogleLogo from '../../../../shared/svgs/google-logo' import GoogleLogo from '../../../../shared/svgs/google-logo'
import OrcidLogo from '../../../../shared/svgs/orcid-logo' import OrcidLogo from '../../../../shared/svgs/orcid-logo'
import LinkingStatus from './status' import LinkingStatus from './status'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5'
const providerLogos: { readonly [p: string]: JSX.Element } = { const providerLogos: { readonly [p: string]: JSX.Element } = {
collabratec: <IEEELogo />, collabratec: <IEEELogo />,
@ -112,28 +114,37 @@ function ActionButton({
const { t } = useTranslation() const { t } = useTranslation()
if (unlinkRequestInflight) { if (unlinkRequestInflight) {
return ( return (
<Button className="btn-danger-ghost" bsStyle={null} disabled> <ButtonWrapper
variant="danger-ghost"
disabled
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
>
{t('unlinking')} {t('unlinking')}
</Button> </ButtonWrapper>
) )
} else if (accountIsLinked) { } else if (accountIsLinked) {
return ( return (
<Button <ButtonWrapper
className="btn-danger-ghost" variant="danger-ghost"
bsStyle={null}
onClick={onUnlinkClick} onClick={onUnlinkClick}
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
> >
{t('unlink')} {t('unlink')}
</Button> </ButtonWrapper>
) )
} else { } else {
return ( return (
<a <ButtonWrapper
variant="secondary"
href={linkPath} href={linkPath}
className="btn btn-secondary-info btn-secondary text-capitalize" bs3Props={{ bsStyle: null }}
className={bsVersion({
bs3: 'btn btn-secondary-info btn-secondary text-capitalize',
bs5: 'text-capitalize',
})}
> >
{t('link')} {t('link')}
</a> </ButtonWrapper>
) )
} }
} }
@ -166,20 +177,23 @@ function UnlinkConfirmModal({
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button <ButtonWrapper
bsStyle={null} variant="secondary"
className="btn-secondary-info btn-secondary"
onClick={handleHide} onClick={handleHide}
bs3Props={{
bsStyle: null,
className: 'btn-secondary-info btn-secondary',
}}
> >
{t('cancel')} {t('cancel')}
</Button> </ButtonWrapper>
<Button <ButtonWrapper
className="btn-danger-ghost" variant="danger-ghost"
bsStyle={null}
onClick={handleConfirmation} onClick={handleConfirmation}
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
> >
{t('unlink')} {t('unlink')}
</Button> </ButtonWrapper>
</Modal.Footer> </Modal.Footer>
</AccessibleModal> </AccessibleModal>
) )

View file

@ -201,7 +201,8 @@ function PasswordForm() {
form="password-change-form" form="password-change-form"
type="submit" type="submit"
variant="primary" variant="primary"
disabled={isLoading || !isFormValid} disabled={!isFormValid}
isLoading={isLoading}
bs3Props={{ bs3Props={{
loading: isLoading ? `${t('saving')}` : t('change'), loading: isLoading ? `${t('saving')}` : t('change'),
}} }}

View file

@ -2,6 +2,7 @@ import MaterialIcon from '@/shared/components/material-icon'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { GroupSSOLinkingStatus } from '../../../../../types/subscription/sso' import { GroupSSOLinkingStatus } from '../../../../../types/subscription/sso'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
function SecuritySection() { function SecuritySection() {
const { t } = useTranslation() const { t } = useTranslation()
@ -84,12 +85,13 @@ function SecuritySection() {
</div> </div>
{linked ? null : ( {linked ? null : (
<div className="button-column"> <div className="button-column">
<a <ButtonWrapper
className="btn btn-primary" variant="primary"
bs3Props={{ className: 'btn btn-primary', bsStyle: null }}
href={`/subscription/${groupId}/sso_enrollment`} href={`/subscription/${groupId}/sso_enrollment`}
> >
{t('set_up_sso')} {t('set_up_sso')}
</a> </ButtonWrapper>
</div> </div>
)} )}
</div> </div>

View file

@ -7,6 +7,7 @@ import Button from '../button'
export type ButtonWrapperProps = ButtonProps & { export type ButtonWrapperProps = ButtonProps & {
bs3Props?: { bs3Props?: {
bsStyle?: string | null bsStyle?: string | null
className?: string
loading?: React.ReactNode loading?: React.ReactNode
} }
} }
@ -26,11 +27,12 @@ export default function ButtonWrapper(props: ButtonWrapperProps) {
const { bs3Props, ...rest } = props const { bs3Props, ...rest } = props
const bs3ButtonProps: BS3ButtonProps = { const bs3ButtonProps: BS3ButtonProps = {
bsStyle: rest.variant, bsStyle: rest.variant === 'secondary' ? 'default' : rest.variant,
bsSize: mapBsButtonSizes(rest.size), bsSize: mapBsButtonSizes(rest.size),
className: rest.className, className: rest.className,
disabled: rest.isLoading || rest.disabled, disabled: rest.isLoading || rest.disabled,
form: rest.form, form: rest.form,
href: rest.href,
onClick: rest.onClick, onClick: rest.onClick,
type: rest.type, type: rest.type,
...bs3Props, ...bs3Props,

View file

@ -19,4 +19,5 @@ export type ButtonProps = {
| 'danger' | 'danger'
| 'danger-ghost' | 'danger-ghost'
| 'premium' | 'premium'
| 'link'
} }

View file

@ -2,12 +2,12 @@
// Use CSS variables for link colors to make it easy to override in marketing page // Use CSS variables for link colors to make it easy to override in marketing page
:root { :root {
--link-color: var(--link-ui-visited); --link-color: var(--link-ui);
--link-hover-color: var(--link-ui-hover); --link-hover-color: var(--link-ui-hover);
--link-visited-color: var(--link-ui-visited); --link-visited-color: var(--link-ui-visited);
} }
a { a:not([role='button']) {
color: var(--link-color); color: var(--link-color);
&:hover { &:hover {

View file

@ -87,10 +87,44 @@
} }
} }
// Link buttons
// -------------------------
// Make a button look and behave like a link
.btn-link {
color: var(--link-ui);
font-weight: normal;
cursor: pointer;
border-radius: 0;
text-decoration: underline;
padding: 0;
font-size: inherit;
vertical-align: inherit;
&,
&:active,
&[disabled],
fieldset[disabled] & {
background-color: transparent;
@include box-shadow(none);
}
&:hover,
&:focus {
color: var(--link-ui-hover);
text-decoration: none;
background-color: transparent;
}
&.btn-danger {
color: var(--content-danger);
}
}
.button-loading { .button-loading {
align-items: center; align-items: center;
display: inline-grid; display: inline-grid;
grid-template-areas: 'container'; // Define a single grid area grid-template-areas: 'container'; // Define a single grid area
pointer-events: none;
} }
.button-loading > * { .button-loading > * {