Merge pull request #20824 from overleaf/ii-bs5-share-modal

[web] BS5 share modal

GitOrigin-RevId: 40a33e06eab720b568d31aefa021682535b6934e
This commit is contained in:
ilkin-overleaf 2024-10-09 15:13:50 +03:00 committed by Copybot
parent 6c7ee8f778
commit 4f838ccacf
44 changed files with 1229 additions and 600 deletions

View file

@ -1,11 +1,13 @@
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import { useUserContext } from '../../../shared/context/user-context' import { useUserContext } from '../../../shared/context/user-context'
import { upgradePlan } from '../../../main/account-upgrade' import { upgradePlan } from '../../../main/account-upgrade'
import StartFreeTrialButton from '../../../shared/components/start-free-trial-button' import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useFeatureFlag } from '../../../shared/context/split-test-context' import { useFeatureFlag } from '../../../shared/context/split-test-context'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function AddCollaboratorsUpgrade() { export default function AddCollaboratorsUpgrade() {
const { t } = useTranslation() const { t } = useTranslation()
@ -21,34 +23,52 @@ export default function AddCollaboratorsUpgrade() {
</p> </p>
<ul className="list-unstyled"> <ul className="list-unstyled">
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('unlimited_projects')} {t('unlimited_projects')}
</li> </li>
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('collabs_per_proj', { {t('collabs_per_proj', {
collabcount: 'Multiple', collabcount: 'Multiple',
})} })}
</li> </li>
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('full_doc_history')} {t('full_doc_history')}
</li> </li>
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('sync_to_dropbox')} {t('sync_to_dropbox')}
</li> </li>
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('sync_to_github')} {t('sync_to_github')}
</li> </li>
<li> <li>
<Icon type="check" /> <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" className="align-text-bottom" />}
/>
&nbsp; &nbsp;
{t('compile_larger_projects')} {t('compile_larger_projects')}
</li> </li>
@ -65,15 +85,15 @@ export default function AddCollaboratorsUpgrade() {
: t('start_free_trial')} : t('start_free_trial')}
</StartFreeTrialButton> </StartFreeTrialButton>
) : ( ) : (
<Button <OLButton
bsStyle="primary" variant="primary"
onClick={() => { onClick={() => {
upgradePlan('project-sharing') upgradePlan('project-sharing')
setStartedFreeTrial(true) setStartedFreeTrial(true)
}} }}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
)} )}
</p> </p>
{startedFreeTrial && ( {startedFreeTrial && (

View file

@ -1,6 +1,5 @@
import { useState, useMemo, useCallback } from 'react' import { useState, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
import { useMultipleSelection } from 'downshift' import { useMultipleSelection } from 'downshift'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import SelectCollaborators from './select-collaborators' import SelectCollaborators from './select-collaborators'
@ -10,6 +9,10 @@ import useIsMounted from '../../../shared/hooks/use-is-mounted'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { sendMB } from '../../../infrastructure/event-tracking' import { sendMB } from '../../../infrastructure/event-tracking'
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function AddCollaborators() { export default function AddCollaborators() {
const [privileges, setPrivileges] = useState('readAndWrite') const [privileges, setPrivileges] = useState('readAndWrite')
@ -139,38 +142,39 @@ export default function AddCollaborators() {
]) ])
return ( return (
<Form> <OLForm>
<FormGroup> <OLFormGroup>
<SelectCollaborators <SelectCollaborators
loading={!nonMemberContacts} loading={!nonMemberContacts}
options={nonMemberContacts || []} options={nonMemberContacts || []}
placeholder="joe@example.com, sue@example.com, …" placeholder="joe@example.com, sue@example.com, …"
multipleSelectionProps={multipleSelectionProps} multipleSelectionProps={multipleSelectionProps}
/> />
</FormGroup> </OLFormGroup>
<FormGroup> <OLFormGroup>
<div className="pull-right"> <div className="pull-right">
<FormControl <OLFormSelect
componentClass="select"
className="privileges" className="privileges"
bsSize="sm"
value={privileges} value={privileges}
onChange={event => setPrivileges(event.target.value)} onChange={event => setPrivileges(event.target.value)}
bs3Props={{
bsSize: 'sm',
}}
> >
<option value="readAndWrite">{t('can_edit')}</option> <option value="readAndWrite">{t('can_edit')}</option>
<option value="readOnly">{t('read_only')}</option> <option value="readOnly">{t('read_only')}</option>
</FormControl> </OLFormSelect>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<ClickableElementEnhancer <ClickableElementEnhancer
as={Button} as={OLButton}
onClick={handleSubmit} onClick={handleSubmit}
bsStyle="primary" variant="primary"
> >
{t('share')} {t('share')}
</ClickableElementEnhancer> </ClickableElementEnhancer>
</div> </div>
</FormGroup> </OLFormGroup>
</Form> </OLForm>
) )
} }

View file

@ -4,11 +4,17 @@ import { useTranslation } from 'react-i18next'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import TransferOwnershipModal from './transfer-ownership-modal' import TransferOwnershipModal from './transfer-ownership-modal'
import { removeMemberFromProject, updateMember } from '../utils/api' import { removeMemberFromProject, updateMember } from '../utils/api'
import { Button, Col, Form, FormControl, FormGroup } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { sendMB } from '../../../infrastructure/event-tracking' import { sendMB } from '../../../infrastructure/event-tracking'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
export default function EditMember({ member }) { export default function EditMember({ member }) {
const [privileges, setPrivileges] = useState(member.privileges) const [privileges, setPrivileges] = useState(member.privileges)
@ -53,20 +59,24 @@ export default function EditMember({ member }) {
} }
return ( return (
<Form horizontal id="share-project-form" onSubmit={handleSubmit}> <form
<FormGroup className="project-member row"> id="share-project-form"
<Col xs={7}> className={bsVersion({ bs3: 'form-horizontal' })}
<FormControl.Static>{member.email}</FormControl.Static> onSubmit={handleSubmit}
</Col> >
<OLFormGroup className="project-member row">
<OLCol xs={7} className={bsVersion({ bs3: 'pt-1', bs5: 'pt-2' })}>
{member.email}
</OLCol>
<Col xs={3}> <OLCol xs={3}>
<SelectPrivilege <SelectPrivilege
value={privileges} value={privileges}
handleChange={event => setPrivileges(event.target.value)} handleChange={event => setPrivileges(event.target.value)}
/> />
</Col> </OLCol>
<Col xs={2}> <OLCol xs={2}>
{privileges === member.privileges ? ( {privileges === member.privileges ? (
<RemoveMemberAction member={member} /> <RemoveMemberAction member={member} />
) : ( ) : (
@ -74,9 +84,9 @@ export default function EditMember({ member }) {
handleReset={() => setPrivileges(member.privileges)} handleReset={() => setPrivileges(member.privileges)}
/> />
)} )}
</Col> </OLCol>
</FormGroup> </OLFormGroup>
</Form> </form>
) )
} }
EditMember.propTypes = { EditMember.propTypes = {
@ -91,17 +101,18 @@ function SelectPrivilege({ value, handleChange }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<FormControl <OLFormSelect
componentClass="select"
className="privileges" className="privileges"
bsSize="sm"
value={value} value={value}
onChange={handleChange} onChange={handleChange}
bs3Props={{
bsSize: 'sm',
}}
> >
<option value="owner">{t('owner')}</option> <option value="owner">{t('owner')}</option>
<option value="readAndWrite">{t('can_edit')}</option> <option value="readAndWrite">{t('can_edit')}</option>
<option value="readOnly">{t('read_only')}</option> <option value="readOnly">{t('read_only')}</option>
</FormControl> </OLFormSelect>
) )
} }
@ -134,23 +145,25 @@ function RemoveMemberAction({ member }) {
} }
return ( return (
<FormControl.Static className="text-center"> <div className="text-center">
<Tooltip <OLTooltip
id="remove-collaborator" id="remove-collaborator"
description={t('remove_collaborator')} description={t('remove_collaborator')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
type="button" variant="link"
bsStyle="link"
onClick={handleClick} onClick={handleClick}
className="remove-button" className="remove-button"
aria-label={t('remove_collaborator')} aria-label={t('remove_collaborator')}
> >
<Icon type="times" /> <BootstrapVersionSwitcher
</Button> bs3={<Icon type="times" />}
</Tooltip> bs5={<MaterialIcon type="clear" />}
</FormControl.Static> />
</OLButton>
</OLTooltip>
</div>
) )
} }
@ -167,15 +180,19 @@ function ChangePrivilegesActions({ handleReset }) {
return ( return (
<div className="text-center"> <div className="text-center">
<Button type="submit" bsSize="sm" bsStyle="primary"> <OLButton type="submit" size="sm" variant="primary">
{t('change_or_cancel-change')} {t('change_or_cancel-change')}
</Button> </OLButton>
<div className="text-sm"> <div className="text-sm">
{t('change_or_cancel-or')} {t('change_or_cancel-or')}
&nbsp; &nbsp;
<Button type="button" className="btn-inline-link" onClick={handleReset}> <OLButton
variant="link"
className="btn-inline-link"
onClick={handleReset}
>
{t('change_or_cancel-cancel')} {t('change_or_cancel-cancel')}
</Button> </OLButton>
</div> </div>
</div> </div>
) )

View file

@ -2,19 +2,25 @@ import { useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { Button, Col, Row } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MemberPrivileges from './member-privileges' import MemberPrivileges from './member-privileges'
import { resendInvite, revokeInvite } from '../utils/api' import { resendInvite, revokeInvite } from '../utils/api'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { sendMB } from '../../../infrastructure/event-tracking' import { sendMB } from '../../../infrastructure/event-tracking'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
export default function Invite({ invite, isProjectOwner }) { export default function Invite({ invite, isProjectOwner }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="project-invite"> <OLRow className="project-invite">
<Col xs={7}> <OLCol xs={7}>
<div>{invite.email}</div> <div>{invite.email}</div>
<div className="small"> <div className="small">
@ -22,18 +28,18 @@ export default function Invite({ invite, isProjectOwner }) {
.&nbsp; .&nbsp;
{isProjectOwner && <ResendInvite invite={invite} />} {isProjectOwner && <ResendInvite invite={invite} />}
</div> </div>
</Col> </OLCol>
<Col xs={3} className="text-left"> <OLCol xs={3} className="text-start">
<MemberPrivileges privileges={invite.privileges} /> <MemberPrivileges privileges={invite.privileges} />
</Col> </OLCol>
{isProjectOwner && ( {isProjectOwner && (
<Col xs={2} className="text-center"> <OLCol xs={2} className="text-center">
<RevokeInvite invite={invite} /> <RevokeInvite invite={invite} />
</Col> </OLCol>
)} )}
</Row> </OLRow>
) )
} }
@ -71,15 +77,15 @@ function ResendInvite({ invite }) {
) )
return ( return (
<Button <OLButton
bsStyle="link" variant="link"
className="btn-inline-link" className="btn-inline-link"
onClick={handleClick} onClick={handleClick}
disabled={inFlight} disabled={inFlight}
// ref={buttonRef} // ref={buttonRef}
> >
{t('resend')} {t('resend')}
</Button> </OLButton>
) )
} }
@ -109,21 +115,26 @@ function RevokeInvite({ invite }) {
} }
return ( return (
<Tooltip <OLTooltip
id="revoke-invite" id="revoke-invite"
description={t('revoke_invite')} description={t('revoke_invite')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
type="button" variant="link"
bsStyle="link"
onClick={handleClick} onClick={handleClick}
aria-label={t('revoke')} aria-label={t('revoke')}
className="btn-inline-link" className={classnames(
'btn-inline-link',
bsVersion({ bs5: 'text-decoration-none' })
)}
> >
<Icon type="times" /> <BootstrapVersionSwitcher
</Button> bs3={<Icon type="times" />}
</Tooltip> bs5={<MaterialIcon type="clear" />}
/>
</OLButton>
</OLTooltip>
) )
} }

View file

@ -1,8 +1,6 @@
import { useCallback, useState, useEffect } from 'react' import { useCallback, useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Button, Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import { setProjectAccessLevel } from '../utils/api' import { setProjectAccessLevel } from '../utils/api'
@ -15,6 +13,12 @@ import { getJSON } from '../../../infrastructure/fetch-json'
import useAbortController from '../../../shared/hooks/use-abort-controller' import useAbortController from '../../../shared/hooks/use-abort-controller'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
export default function LinkSharing() { export default function LinkSharing() {
const [inflight, setInflight] = useState(false) const [inflight, setInflight] = useState(false)
@ -84,13 +88,12 @@ export default function LinkSharing() {
function PrivateSharing({ setAccessLevel, inflight, projectId }) { function PrivateSharing({ setAccessLevel, inflight, projectId }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
{t('link_sharing_is_off')} {t('link_sharing_is_off')}
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
type="button" variant="link"
bsStyle="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => { onClick={() => {
setAccessLevel('tokenBased') setAccessLevel('tokenBased')
@ -99,11 +102,11 @@ function PrivateSharing({ setAccessLevel, inflight, projectId }) {
disabled={inflight} disabled={inflight}
> >
{t('turn_on_link_sharing')} {t('turn_on_link_sharing')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -128,22 +131,22 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
}, [projectId, signal]) }, [projectId, signal])
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
<strong>{t('link_sharing_is_on')}</strong> <strong>{t('link_sharing_is_on')}</strong>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
bsStyle="link" variant="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => setAccessLevel('private')} onClick={() => setAccessLevel('private')}
disabled={inflight} disabled={inflight}
> >
{t('turn_off_link_sharing')} {t('turn_off_link_sharing')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
</Col> </OLCol>
<Col xs={12} className="access-token-display-area"> <OLCol xs={12} className="access-token-display-area">
<div className="access-token-wrapper"> <div className="access-token-wrapper">
<strong>{t('anyone_with_link_can_edit')}</strong> <strong>{t('anyone_with_link_can_edit')}</strong>
<AccessToken <AccessToken
@ -162,8 +165,8 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
tooltipId="tooltip-copy-link-ro" tooltipId="tooltip-copy-link-ro"
/> />
</div> </div>
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -176,26 +179,25 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
<strong> <strong>
{accessLevel === 'readAndWrite' && t('this_project_is_public')} {accessLevel === 'readAndWrite' && t('this_project_is_public')}
{accessLevel === 'readOnly' && t('this_project_is_public_read_only')} {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
</strong> </strong>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
type="button" variant="link"
bsStyle="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => setAccessLevel('private')} onClick={() => setAccessLevel('private')}
disabled={inflight} disabled={inflight}
> >
{t('make_private')} {t('make_private')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -220,8 +222,8 @@ export function ReadOnlyTokenLink() {
}, [projectId, signal]) }, [projectId, signal])
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="access-token-display-area"> <OLCol className="access-token-display-area">
<div className="access-token-wrapper"> <div className="access-token-wrapper">
<strong>{t('anyone_with_link_can_view')}</strong> <strong>{t('anyone_with_link_can_view')}</strong>
<AccessToken <AccessToken
@ -231,8 +233,8 @@ export function ReadOnlyTokenLink() {
tooltipId="tooltip-copy-link-ro" tooltipId="tooltip-copy-link-ro"
/> />
</div> </div>
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -275,7 +277,7 @@ function LinkSharingInfo() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Tooltip <OLTooltip
id="link-sharing-info" id="link-sharing-info"
description={t('learn_more_about_link_sharing')} description={t('learn_more_about_link_sharing')}
> >
@ -284,8 +286,11 @@ function LinkSharingInfo() {
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
<Icon type="question-circle" /> <BootstrapVersionSwitcher
bs3={<Icon type="question-circle" />}
bs5={<MaterialIcon type="help" className="align-middle" />}
/>
</a> </a>
</Tooltip> </OLTooltip>
) )
} }

View file

@ -1,17 +1,18 @@
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
export default function OwnerInfo() { export default function OwnerInfo() {
const { t } = useTranslation() const { t } = useTranslation()
const { owner } = useProjectContext() const { owner } = useProjectContext()
return ( return (
<Row className="project-member"> <OLRow className="project-member">
<Col xs={7}>{owner?.email}</Col> <OLCol xs={7}>{owner?.email}</OLCol>
<Col xs={3} className="text-left"> <OLCol xs={3} className="text-start">
{t('owner')} {t('owner')}
</Col> </OLCol>
</Row> </OLRow>
) )
} }

View file

@ -1,11 +1,11 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import { upgradePlan } from '../../../../main/account-upgrade' import { upgradePlan } from '../../../../main/account-upgrade'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { useUserContext } from '@/shared/context/user-context' import { useUserContext } from '@/shared/context/user-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import StartFreeTrialButton from '@/shared/components/start-free-trial-button' import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
import OLButton from '@/features/ui/components/ol/ol-button'
type AccessLevelsChangedProps = { type AccessLevelsChangedProps = {
somePendingEditorsResolved: boolean somePendingEditorsResolved: boolean
@ -50,20 +50,20 @@ export default function AccessLevelsChanged({
{t('upgrade')} {t('upgrade')}
</StartFreeTrialButton> </StartFreeTrialButton>
) : ( ) : (
<Button <OLButton
bsSize="sm" variant="secondary"
className="btn-secondary" size="sm"
onClick={() => { onClick={() => {
upgradePlan('project-sharing') upgradePlan('project-sharing')
}} }}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
)} )}
<Button <OLButton
variant="link"
size="sm"
href="https://www.overleaf.com/blog/changes-to-project-sharing" href="https://www.overleaf.com/blog/changes-to-project-sharing"
bsSize="sm"
className="btn-link"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={() => { onClick={() => {
@ -75,7 +75,7 @@ export default function AccessLevelsChanged({
}} }}
> >
{t('read_more')} {t('read_more')}
</Button> </OLButton>
</div> </div>
} }
/> />

View file

@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import { upgradePlan } from '../../../../main/account-upgrade' import { upgradePlan } from '../../../../main/account-upgrade'
import { linkSharingEnforcementDate } from '../../utils/link-sharing' import { linkSharingEnforcementDate } from '../../utils/link-sharing'
@ -7,6 +6,7 @@ import { useProjectContext } from '@/shared/context/project-context'
import { useUserContext } from '@/shared/context/user-context' import { useUserContext } from '@/shared/context/user-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import StartFreeTrialButton from '@/shared/components/start-free-trial-button' import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function AddCollaboratorsUpgrade() { export default function AddCollaboratorsUpgrade() {
const { t } = useTranslation() const { t } = useTranslation()
@ -40,20 +40,20 @@ export default function AddCollaboratorsUpgrade() {
{t('upgrade')} {t('upgrade')}
</StartFreeTrialButton> </StartFreeTrialButton>
) : ( ) : (
<Button <OLButton
bsSize="sm" variant="secondary"
className="btn-secondary" size="sm"
onClick={() => { onClick={() => {
upgradePlan('project-sharing') upgradePlan('project-sharing')
}} }}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
)} )}
<Button <OLButton
variant="link"
size="sm"
href="https://www.overleaf.com/blog/changes-to-project-sharing" href="https://www.overleaf.com/blog/changes-to-project-sharing"
bsSize="sm"
className="btn-link"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={() => { onClick={() => {
@ -65,7 +65,7 @@ export default function AddCollaboratorsUpgrade() {
}} }}
> >
{t('read_more')} {t('read_more')}
</Button> </OLButton>
</div> </div>
} }
/> />

View file

@ -1,6 +1,5 @@
import { useEffect, useState, useMemo, useCallback } from 'react' import { useEffect, useState, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
import { useMultipleSelection } from 'downshift' import { useMultipleSelection } from 'downshift'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import SelectCollaborators from './select-collaborators' import SelectCollaborators from './select-collaborators'
@ -11,6 +10,10 @@ import { useProjectContext } from '@/shared/context/project-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function AddCollaborators({ readOnly }) { export default function AddCollaborators({ readOnly }) {
const [privileges, setPrivileges] = useState('readAndWrite') const [privileges, setPrivileges] = useState('readAndWrite')
@ -146,41 +149,42 @@ export default function AddCollaborators({ readOnly }) {
]) ])
return ( return (
<Form className="add-collabs"> <OLForm className="add-collabs">
<FormGroup> <OLFormGroup>
<SelectCollaborators <SelectCollaborators
loading={!nonMemberContacts} loading={!nonMemberContacts}
options={nonMemberContacts || []} options={nonMemberContacts || []}
placeholder="Email, comma separated" placeholder="Email, comma separated"
multipleSelectionProps={multipleSelectionProps} multipleSelectionProps={multipleSelectionProps}
/> />
</FormGroup> </OLFormGroup>
<FormGroup> <OLFormGroup>
<div className="pull-right"> <div className="pull-right">
<FormControl <OLFormSelect
componentClass="select"
className="privileges" className="privileges"
bsSize="sm"
value={privileges} value={privileges}
onChange={event => setPrivileges(event.target.value)} onChange={event => setPrivileges(event.target.value)}
bs3Props={{
bsSize: 'sm',
}}
> >
<option disabled={readOnly} value="readAndWrite"> <option disabled={readOnly} value="readAndWrite">
{t('can_edit')} {t('can_edit')}
</option> </option>
<option value="readOnly">{t('can_view')}</option> <option value="readOnly">{t('can_view')}</option>
</FormControl> </OLFormSelect>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<ClickableElementEnhancer <ClickableElementEnhancer
as={Button} as={OLButton}
onClick={handleSubmit} onClick={handleSubmit}
bsStyle="primary" variant="primary"
> >
{t('invite')} {t('invite')}
</ClickableElementEnhancer> </ClickableElementEnhancer>
</div> </div>
</FormGroup> </OLFormGroup>
</Form> </OLForm>
) )
} }

View file

@ -1,4 +1,3 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import { upgradePlan } from '@/main/account-upgrade' import { upgradePlan } from '@/main/account-upgrade'
@ -6,6 +5,7 @@ import { useProjectContext } from '@/shared/context/project-context'
import { useUserContext } from '@/shared/context/user-context' import { useUserContext } from '@/shared/context/user-context'
import StartFreeTrialButton from '@/shared/components/start-free-trial-button' import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function CollaboratorsLimitUpgrade() { export default function CollaboratorsLimitUpgrade() {
const { t } = useTranslation() const { t } = useTranslation()
@ -44,15 +44,14 @@ export default function CollaboratorsLimitUpgrade() {
{t('upgrade')} {t('upgrade')}
</StartFreeTrialButton> </StartFreeTrialButton>
) : ( ) : (
<Button <OLButton
bsSize="medium" variant="premium"
className="btn-premium"
onClick={() => { onClick={() => {
upgradePlan('project-sharing') upgradePlan('project-sharing')
}} }}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
) )
} }
/> />
@ -78,15 +77,15 @@ export default function CollaboratorsLimitUpgrade() {
{t('upgrade')} {t('upgrade')}
</StartFreeTrialButton> </StartFreeTrialButton>
) : ( ) : (
<Button <OLButton
bsSize="sm" size="sm"
className="btn-secondary" variant="secondary"
onClick={() => { onClick={() => {
upgradePlan('project-sharing') upgradePlan('project-sharing')
}} }}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
) )
} }
/> />

View file

@ -1,10 +1,9 @@
import { useState, useEffect, useMemo, MouseEventHandler } from 'react' import { useState, useEffect, useMemo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import TransferOwnershipModal from './transfer-ownership-modal' import TransferOwnershipModal from './transfer-ownership-modal'
import { removeMemberFromProject, updateMember } from '../../utils/api' import { removeMemberFromProject, updateMember } from '../../utils/api'
import { Button, Col, Form, FormGroup } from 'react-bootstrap'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
@ -12,6 +11,13 @@ import { Select } from '@/shared/components/select'
import type { ProjectContextMember } from '@/shared/context/types/project-context' import type { ProjectContextMember } from '@/shared/context/types/project-context'
import { PermissionsLevel } from '@/features/ide-react/types/permissions' import { PermissionsLevel } from '@/features/ide-react/types/permissions'
import { linkSharingEnforcementDate } from '../../utils/link-sharing' import { linkSharingEnforcementDate } from '../../utils/link-sharing'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLCol from '@/features/ui/components/ol/ol-col'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
@ -115,23 +121,45 @@ export default function EditMember({
} }
return ( return (
<Form <form
horizontal className={bsVersion({ bs3: 'form-horizontal' })}
id="share-project-form" id="share-project-form"
onSubmit={e => { onSubmit={e => {
e.preventDefault() e.preventDefault()
commitPrivilegeChange(privileges) commitPrivilegeChange(privileges)
}} }}
> >
<FormGroup className="project-member"> <OLFormGroup
<Col xs={7}> className={classnames('project-member', bsVersion({ bs5: 'row' }))}
>
<OLCol xs={7}>
<div className="project-member-email-icon"> <div className="project-member-email-icon">
<BootstrapVersionSwitcher
bs3={
<Icon <Icon
type={ type={
shouldWarnMember() || member.pendingEditor ? 'warning' : 'user' shouldWarnMember() || member.pendingEditor
? 'warning'
: 'user'
} }
fw fw
/> />
}
bs5={
<MaterialIcon
type={
shouldWarnMember() || member.pendingEditor
? 'warning'
: 'person'
}
className={
shouldWarnMember() || member.pendingEditor
? 'text-warning'
: undefined
}
/>
}
/>
<div className="email-warning"> <div className="email-warning">
{member.email} {member.email}
{member.pendingEditor && ( {member.pendingEditor && (
@ -146,18 +174,23 @@ export default function EditMember({
)} )}
</div> </div>
</div> </div>
</Col> </OLCol>
<Col xs={1}> <OLCol xs={2}>
{privileges !== member.privileges && privilegeChangePending && ( {privileges !== member.privileges && privilegeChangePending && (
<ChangePrivilegesActions <ChangePrivilegesActions
handleReset={() => setPrivileges(member.privileges)} handleReset={() => setPrivileges(member.privileges)}
/> />
)} )}
</Col> </OLCol>
<Col xs={4} className="project-member-select"> <OLCol xs={3} className="project-member-select">
{hasBeenDowngraded && <Icon type="warning" fw />} {hasBeenDowngraded && (
<BootstrapVersionSwitcher
bs3={<Icon type="warning" fw />}
bs5={<MaterialIcon type="warning" className="text-warning" />}
/>
)}
<SelectPrivilege <SelectPrivilege
value={privileges} value={privileges}
@ -169,9 +202,9 @@ export default function EditMember({
hasBeenDowngraded={hasBeenDowngraded} hasBeenDowngraded={hasBeenDowngraded}
canAddCollaborators={canAddCollaborators} canAddCollaborators={canAddCollaborators}
/> />
</Col> </OLCol>
</FormGroup> </OLFormGroup>
</Form> </form>
) )
} }
EditMember.propTypes = { EditMember.propTypes = {
@ -262,8 +295,9 @@ function SelectPrivilege({
} }
type ChangePrivilegesActionsProps = { type ChangePrivilegesActionsProps = {
handleReset: MouseEventHandler<Button> handleReset: React.ComponentProps<typeof OLButton>['onClick']
} }
function ChangePrivilegesActions({ function ChangePrivilegesActions({
handleReset, handleReset,
}: ChangePrivilegesActionsProps) { }: ChangePrivilegesActionsProps) {
@ -271,15 +305,19 @@ function ChangePrivilegesActions({
return ( return (
<div className="text-center"> <div className="text-center">
<Button type="submit" bsSize="sm" bsStyle="primary"> <OLButton type="submit" size="sm" variant="primary">
{t('change_or_cancel-change')} {t('change_or_cancel-change')}
</Button> </OLButton>
<div className="text-sm"> <div className="text-sm">
{t('change_or_cancel-or')} {t('change_or_cancel-or')}
&nbsp; &nbsp;
<Button type="button" className="btn-inline-link" onClick={handleReset}> <OLButton
variant="link"
className="btn-inline-link"
onClick={handleReset}
>
{t('change_or_cancel-cancel')} {t('change_or_cancel-cancel')}
</Button> </OLButton>
</div> </div>
</div> </div>
) )

View file

@ -1,7 +1,13 @@
import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { linkSharingEnforcementDate } from '../../utils/link-sharing' import { linkSharingEnforcementDate } from '../../utils/link-sharing'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import OLButton from '@/features/ui/components/ol/ol-button'
import {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
type EditorOverLimitModalContentProps = { type EditorOverLimitModalContentProps = {
handleHide: () => void handleHide: () => void
@ -14,22 +20,21 @@ export default function EditorOverLimitModalContent({
return ( return (
<> <>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('do_you_need_edit_access')}</Modal.Title> <OLModalTitle>{t('do_you_need_edit_access')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<p> <p>
{t('this_project_has_more_than_max_collabs', { {t('this_project_has_more_than_max_collabs', {
linkSharingDate: linkSharingEnforcementDate, linkSharingDate: linkSharingEnforcementDate,
})} })}
</p> </p>
<p>{t('to_keep_edit_access')}</p> <p>{t('to_keep_edit_access')}</p>
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button <OLButton
bsStyle={null} variant="secondary"
className="btn-secondary"
href="/blog/changes-to-project-sharing" href="/blog/changes-to-project-sharing"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -41,9 +46,9 @@ export default function EditorOverLimitModalContent({
}} }}
> >
{t('learn_more')} {t('learn_more')}
</Button> </OLButton>
<Button <OLButton
className="btn-primary" variant="primary"
onClick={() => { onClick={() => {
sendMB('notification-click', { sendMB('notification-click', {
name: 'link-sharing-collaborator-limit', name: 'link-sharing-collaborator-limit',
@ -53,8 +58,8 @@ export default function EditorOverLimitModalContent({
}} }}
> >
{t('ok')} {t('ok')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</> </>
) )
} }

View file

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import AccessibleModal from '@/shared/components/accessible-modal'
import EditorOverLimitModalContent from './editor-over-limit-modal-content' import EditorOverLimitModalContent from './editor-over-limit-modal-content'
import customLocalStorage from '@/infrastructure/local-storage' import customLocalStorage from '@/infrastructure/local-storage'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { useEditorContext } from '@/shared/context/editor-context' import { useEditorContext } from '@/shared/context/editor-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import OLModal from '@/features/ui/components/ol/ol-modal'
const EditorOverLimitModal = () => { const EditorOverLimitModal = () => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
@ -55,7 +55,7 @@ const EditorOverLimitModal = () => {
}, [features, isProjectOwner, members, permissionsLevel, projectId]) }, [features, isProjectOwner, members, permissionsLevel, projectId])
return show ? ( return show ? (
<AccessibleModal <OLModal
animation animation
show={show} show={show}
onHide={() => { onHide={() => {
@ -67,7 +67,7 @@ const EditorOverLimitModal = () => {
id="editor-over-limit-modal" id="editor-over-limit-modal"
> >
<EditorOverLimitModalContent handleHide={handleHide} /> <EditorOverLimitModalContent handleHide={handleHide} />
</AccessibleModal> </OLModal>
) : null ) : null
} }

View file

@ -2,37 +2,43 @@ import { useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { Button, Col, Row } from 'react-bootstrap'
import Tooltip from '@/shared/components/tooltip'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MemberPrivileges from './member-privileges' import MemberPrivileges from './member-privileges'
import { resendInvite, revokeInvite } from '../../utils/api' import { resendInvite, revokeInvite } from '../../utils/api'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
export default function Invite({ invite, isProjectOwner }) { export default function Invite({ invite, isProjectOwner }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="project-invite"> <OLRow className="project-invite">
<Col xs={8}> <OLCol xs={8}>
<div>{invite.email}</div> <div>{invite.email}</div>
<div className="small"> <div className="small">
{t('invite_not_accepted')} {t('invite_not_accepted')}
.&nbsp; .&nbsp;
{isProjectOwner && <ResendInvite invite={invite} />} {isProjectOwner && <ResendInvite invite={invite} />}
</div> </div>
</Col> </OLCol>
<Col xs={3} className="text-right"> <OLCol xs={3} className="text-end">
<MemberPrivileges privileges={invite.privileges} /> <MemberPrivileges privileges={invite.privileges} />
</Col> </OLCol>
{isProjectOwner && ( {isProjectOwner && (
<Col xs={1} className="text-center"> <OLCol xs={1} className="text-center">
<RevokeInvite invite={invite} /> <RevokeInvite invite={invite} />
</Col> </OLCol>
)} )}
</Row> </OLRow>
) )
} }
@ -70,15 +76,15 @@ function ResendInvite({ invite }) {
) )
return ( return (
<Button <OLButton
bsStyle="link" variant="link"
className="btn-inline-link" className="btn-inline-link"
onClick={handleClick} onClick={handleClick}
disabled={inFlight} disabled={inFlight}
// ref={buttonRef} // ref={buttonRef}
> >
{t('resend')} {t('resend')}
</Button> </OLButton>
) )
} }
@ -108,21 +114,26 @@ function RevokeInvite({ invite }) {
} }
return ( return (
<Tooltip <OLTooltip
id="revoke-invite" id="revoke-invite"
description={t('revoke_invite')} description={t('revoke_invite')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
type="button" variant="link"
bsStyle="link"
onClick={handleClick} onClick={handleClick}
aria-label={t('revoke')} aria-label={t('revoke')}
className="btn-inline-link" className={classnames(
'btn-inline-link',
bsVersion({ bs5: 'text-decoration-none' })
)}
> >
<Icon type="times" /> <BootstrapVersionSwitcher
</Button> bs3={<Icon type="times" />}
</Tooltip> bs5={<MaterialIcon type="clear" />}
/>
</OLButton>
</OLTooltip>
) )
} }

View file

@ -1,8 +1,6 @@
import { useCallback, useState, useEffect } from 'react' import { useCallback, useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Button, Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Tooltip from '@/shared/components/tooltip'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { useShareProjectContext } from './share-project-modal' import { useShareProjectContext } from './share-project-modal'
import { setProjectAccessLevel } from '../../utils/api' import { setProjectAccessLevel } from '../../utils/api'
@ -15,6 +13,12 @@ import { getJSON } from '../../../../infrastructure/fetch-json'
import useAbortController from '@/shared/hooks/use-abort-controller' import useAbortController from '@/shared/hooks/use-abort-controller'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
export default function LinkSharing() { export default function LinkSharing() {
const [inflight, setInflight] = useState(false) const [inflight, setInflight] = useState(false)
@ -88,13 +92,12 @@ export default function LinkSharing() {
function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) { function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
<strong>{t('link_sharing_is_off_short')}</strong> <strong>{t('link_sharing_is_off_short')}</strong>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
type="button" variant="link"
bsStyle="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => { onClick={() => {
setAccessLevel('tokenBased') setAccessLevel('tokenBased')
@ -104,11 +107,11 @@ function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
disabled={inflight} disabled={inflight}
> >
{t('turn_on_link_sharing')} {t('turn_on_link_sharing')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -139,31 +142,37 @@ function TokenBasedSharing({
}, [projectId, signal]) }, [projectId, signal])
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
<strong>{t('link_sharing_is_on')}</strong> <strong>{t('link_sharing_is_on')}</strong>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
bsStyle="link" variant="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => setAccessLevel('private')} onClick={() => setAccessLevel('private')}
disabled={inflight} disabled={inflight}
> >
{t('turn_off_link_sharing')} {t('turn_off_link_sharing')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
<Button <OLButton
bsStyle="link" variant="link"
className="btn-chevron" className="btn-chevron align-middle"
onClick={() => setShowLinks(!showLinks)} onClick={() => setShowLinks(!showLinks)}
> >
<Icon type={showLinks ? 'chevron-up' : 'chevron-down'} fw /> <BootstrapVersionSwitcher
</Button> bs3={<Icon type={showLinks ? 'chevron-up' : 'chevron-down'} fw />}
</Col> bs5={
<MaterialIcon
type={showLinks ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
/>
}
/>
</OLButton>
</OLCol>
{showLinks && ( {showLinks && (
<> <OLCol xs={12} className="access-token-display-area">
<Col xs={12} className="access-token-display-area">
<div className="access-token-wrapper"> <div className="access-token-wrapper">
<strong>{t('anyone_with_link_can_edit')}</strong> <strong>{t('anyone_with_link_can_edit')}</strong>
<AccessToken <AccessToken
@ -182,10 +191,9 @@ function TokenBasedSharing({
tooltipId="tooltip-copy-link-ro" tooltipId="tooltip-copy-link-ro"
/> />
</div> </div>
</Col> </OLCol>
</>
)} )}
</Row> </OLRow>
) )
} }
@ -200,26 +208,25 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="text-center"> <OLCol xs={12} className="text-center">
<strong> <strong>
{accessLevel === 'readAndWrite' && t('this_project_is_public')} {accessLevel === 'readAndWrite' && t('this_project_is_public')}
{accessLevel === 'readOnly' && t('this_project_is_public_read_only')} {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
</strong> </strong>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<Button <OLButton
type="button" variant="link"
bsStyle="link"
className="btn-inline-link" className="btn-inline-link"
onClick={() => setAccessLevel('private')} onClick={() => setAccessLevel('private')}
disabled={inflight} disabled={inflight}
> >
{t('make_private')} {t('make_private')}
</Button> </OLButton>
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<LinkSharingInfo /> <LinkSharingInfo />
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -244,8 +251,8 @@ export function ReadOnlyTokenLink() {
}, [projectId, signal]) }, [projectId, signal])
return ( return (
<Row className="public-access-level"> <OLRow className="public-access-level">
<Col xs={12} className="access-token-display-area"> <OLCol className="access-token-display-area">
<div className="access-token-wrapper"> <div className="access-token-wrapper">
<strong>{t('anyone_with_link_can_view')}</strong> <strong>{t('anyone_with_link_can_view')}</strong>
<AccessToken <AccessToken
@ -255,8 +262,8 @@ export function ReadOnlyTokenLink() {
tooltipId="tooltip-copy-link-ro" tooltipId="tooltip-copy-link-ro"
/> />
</div> </div>
</Col> </OLCol>
</Row> </OLRow>
) )
} }
@ -299,7 +306,7 @@ function LinkSharingInfo() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Tooltip <OLTooltip
id="link-sharing-info" id="link-sharing-info"
description={t('learn_more_about_link_sharing')} description={t('learn_more_about_link_sharing')}
> >
@ -308,8 +315,11 @@ function LinkSharingInfo() {
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
<Icon type="question-circle" /> <BootstrapVersionSwitcher
bs3={<Icon type="question-circle" />}
bs5={<MaterialIcon type="help" className="align-middle" />}
/>
</a> </a>
</Tooltip> </OLTooltip>
) )
} }

View file

@ -1,23 +1,29 @@
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
export default function OwnerInfo() { export default function OwnerInfo() {
const { t } = useTranslation() const { t } = useTranslation()
const { owner } = useProjectContext() const { owner } = useProjectContext()
return ( return (
<Row className="project-member"> <OLRow className="project-member">
<Col xs={8}> <OLCol xs={8}>
<div className="project-member-email-icon"> <div className="project-member-email-icon">
<Icon type="user" fw /> <BootstrapVersionSwitcher
bs3={<Icon type="user" fw />}
bs5={<MaterialIcon type="person" />}
/>
<div className="email-warning">{owner?.email}</div> <div className="email-warning">{owner?.email}</div>
</div> </div>
</Col> </OLCol>
<Col xs={4} className="text-right"> <OLCol xs={4} className="text-end">
{t('owner')} {t('owner')}
</Col> </OLCol>
</Row> </OLRow>
) )
} }

View file

@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
import classnames from 'classnames' import classnames from 'classnames'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import Tag from '@/features/ui/components/bootstrap-5/tag'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
// Unicode characters in these Unicode groups: // Unicode characters in these Unicode groups:
// "General Punctuation Spaces" // "General Punctuation Spaces"
@ -156,13 +162,28 @@ export default function SelectCollaborators({
{t('add_people')} {t('add_people')}
&nbsp; &nbsp;
</strong> </strong>
{loading && <Icon type="refresh" spin />} {loading && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)}
</label> </label>
<div className="host"> <div className="host">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<div {...getComboboxProps()} className="tags" onClick={focusInput}> <div
<div className="tags-main"> {...getComboboxProps()}
className="tags form-control"
onClick={focusInput}
>
{selectedItems.map((selectedItem, index) => ( {selectedItems.map((selectedItem, index) => (
<SelectedItem <SelectedItem
key={`selected-item-${index}`} key={`selected-item-${index}`}
@ -177,8 +198,7 @@ export default function SelectCollaborators({
<input <input
{...getInputProps( {...getInputProps(
getDropdownProps({ getDropdownProps({
className: classnames({ className: classnames('input', {
input: true,
'invalid-tag': !isValidInput, 'invalid-tag': !isValidInput,
}), }),
type: 'email', type: 'email',
@ -244,10 +264,21 @@ export default function SelectCollaborators({
)} )}
/> />
</div> </div>
</div>
<div className={classnames({ autocomplete: isOpen })}> <div
<ul {...getMenuProps()} className="suggestion-list"> className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
>
<ul
{...getMenuProps()}
className={classnames(
bsVersion({
bs3: 'suggestion-list',
bs5: classnames('dropdown-menu select-dropdown-menu', {
show: isOpen,
}),
})
)}
>
{isOpen && {isOpen &&
filteredOptions.map((item, index) => ( filteredOptions.map((item, index) => (
<Option <Option
@ -280,15 +311,36 @@ SelectCollaborators.propTypes = {
function Option({ selected, item, getItemProps, index }) { function Option({ selected, item, getItemProps, index }) {
return ( return (
<li <li
className={classnames('suggestion-item', { selected })} className={bsVersion({
bs3: classnames('suggestion-item', { selected }),
})}
{...getItemProps({ item, index })} {...getItemProps({ item, index })}
> >
<BootstrapVersionSwitcher
bs3={
<>
<Icon type="user" fw /> <Icon type="user" fw />
&nbsp; &nbsp;
{item.display} {item.display}
</>
}
bs5={
<DropdownItem
as="span"
role={undefined}
leadingIcon="person"
className={classnames({
active: selected,
})}
>
{item.display}
</DropdownItem>
}
/>
</li> </li>
) )
} }
Option.propTypes = { Option.propTypes = {
selected: PropTypes.bool.isRequired, selected: PropTypes.bool.isRequired,
item: PropTypes.shape({ item: PropTypes.shape({
@ -318,6 +370,8 @@ function SelectedItem({
) )
return ( return (
<BootstrapVersionSwitcher
bs3={
<span <span
className="tag-item" className="tag-item"
{...getSelectedItemProps({ selectedItem, index })} {...getSelectedItemProps({ selectedItem, index })}
@ -333,8 +387,27 @@ function SelectedItem({
<Icon type="close" fw /> <Icon type="close" fw />
</button> </button>
</span> </span>
}
bs5={
<Tag
prepend={
<BootstrapVersionSwitcher
bs3={<Icon type="user" fw />}
bs5={<MaterialIcon type="person" />}
/>
}
closeBtnProps={{
onClick: handleClick,
}}
{...getSelectedItemProps({ selectedItem, index })}
>
{selectedItem.display}
</Tag>
}
/>
) )
} }
SelectedItem.propTypes = { SelectedItem.propTypes = {
focusInput: PropTypes.func.isRequired, focusInput: PropTypes.func.isRequired,
removeSelectedItem: PropTypes.func.isRequired, removeSelectedItem: PropTypes.func.isRequired,

View file

@ -1,9 +1,10 @@
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import { PublicAccessLevel } from '../../../../../../types/public-access-level' import { PublicAccessLevel } from '../../../../../../types/public-access-level'
import { useEditorContext } from '@/shared/context/editor-context' import { useEditorContext } from '@/shared/context/editor-context'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
export default function SendInvitesNotice() { export default function SendInvitesNotice() {
const { publicAccessLevel } = useProjectContext() const { publicAccessLevel } = useProjectContext()
@ -25,11 +26,11 @@ export default function SendInvitesNotice() {
} }
/> />
)} )}
<Row className="public-access-level public-access-level--notice"> <OLRow className="public-access-level public-access-level-notice">
<Col xs={12} className="text-center"> <OLCol className="text-center">
<AccessLevel level={publicAccessLevel} /> <AccessLevel level={publicAccessLevel} />
</Col> </OLCol>
</Row> </OLRow>
</div> </div>
) )
} }

View file

@ -1,9 +1,9 @@
import { Row } from 'react-bootstrap'
import AddCollaborators from './add-collaborators' import AddCollaborators from './add-collaborators'
import AddCollaboratorsUpgrade from './add-collaborators-upgrade' import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade' import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade'
import AccessLevelsChanged from './access-levels-changed' import AccessLevelsChanged from './access-levels-changed'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import OLRow from '@/features/ui/components/ol/ol-row'
export default function SendInvites({ export default function SendInvites({
canAddCollaborators, canAddCollaborators,
@ -12,7 +12,7 @@ export default function SendInvites({
somePendingEditorsResolved, somePendingEditorsResolved,
}) { }) {
return ( return (
<Row className="invite-controls"> <OLRow className="invite-controls">
{hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && ( {hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && (
<AddCollaboratorsUpgrade /> <AddCollaboratorsUpgrade />
)} )}
@ -27,7 +27,7 @@ export default function SendInvites({
!hasExceededCollaboratorLimit && !hasExceededCollaboratorLimit &&
!haveAnyEditorsBeenDowngraded && <CollaboratorsLimitUpgrade />} !haveAnyEditorsBeenDowngraded && <CollaboratorsLimitUpgrade />}
<AddCollaborators readOnly={!canAddCollaborators} /> <AddCollaborators readOnly={!canAddCollaborators} />
</Row> </OLRow>
) )
} }

View file

@ -1,11 +1,20 @@
import { Button, Modal, Grid } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useEditorContext } from '@/shared/context/editor-context' import { useEditorContext } from '@/shared/context/editor-context'
import { lazy, Suspense } from 'react' import { lazy, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
const ReadOnlyTokenLink = lazy(() => const ReadOnlyTokenLink = lazy(() =>
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({ import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
const { isRestrictedTokenMember } = useEditorContext() const { isRestrictedTokenMember } = useEditorContext()
return ( return (
<AccessibleModal show={show} onHide={cancel} animation={animation}> <OLModal show={show} onHide={cancel} animation={animation}>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('share_project')}</Modal.Title> <OLModalTitle>{t('share_project')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body className="modal-body-share modal-link-share-new"> <OLModalBody className="modal-body-share modal-link-share-new">
<Grid fluid> <div className="container-fluid">
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}> <Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
{isRestrictedTokenMember ? ( {isRestrictedTokenMember ? (
<ReadOnlyTokenLink /> <ReadOnlyTokenLink />
@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
<ShareModalBody /> <ShareModalBody />
)} )}
</Suspense> </Suspense>
</Grid>
</Modal.Body>
<Modal.Footer className="modal-footer-share">
<div className="modal-footer-left">
{inFlight && <Icon type="refresh" spin />}
{error && ( {error && (
<span className="text-danger error"> <OLNotification
<ErrorMessage error={error} /> type="error"
</span> content={<ErrorMessage error={error} />}
className="mb-0 mt-3"
/>
)}
</div>
</OLModalBody>
<OLModalFooter>
<div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
{inFlight && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)} )}
</div> </div>
<div className="modal-footer-right">
<ClickableElementEnhancer <ClickableElementEnhancer
onClick={cancel} onClick={cancel}
as={Button} as={OLButton}
type="button" variant="secondary"
bsStyle={null}
className="btn-secondary"
disabled={inFlight} disabled={inFlight}
> >
{t('close')} {t('close')}
</ClickableElementEnhancer> </ClickableElementEnhancer>
</div> </OLModalFooter>
</Modal.Footer> </OLModal>
</AccessibleModal>
) )
} }

View file

@ -1,12 +1,21 @@
import { useState } from 'react' import { useState } from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { transferProjectOwnership } from '../../utils/api' import { transferProjectOwnership } from '../../utils/api'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { useLocation } from '@/shared/hooks/use-location' import { useLocation } from '@/shared/hooks/use-location'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
export default function TransferOwnershipModal({ member, cancel }) { export default function TransferOwnershipModal({ member, cancel }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
} }
return ( return (
<AccessibleModal show onHide={cancel}> <OLModal show onHide={cancel}>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('change_project_owner')}</Modal.Title> <OLModalTitle>{t('change_project_owner')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<p> <p>
<Trans <Trans
i18nKey="project_ownership_transfer_confirmation_1" i18nKey="project_ownership_transfer_confirmation_1"
@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
/> />
</p> </p>
<p>{t('project_ownership_transfer_confirmation_2')}</p> <p>{t('project_ownership_transfer_confirmation_2')}</p>
</Modal.Body>
<Modal.Footer>
<div className="modal-footer-left">
{inflight && <Icon type="refresh" spin />}
{error && ( {error && (
<span className="text-danger"> <OLNotification
{t('generic_something_went_wrong')} type="error"
</span> content={t('generic_something_went_wrong')}
className="mb-0 mt-3"
/>
)}
</OLModalBody>
<OLModalFooter>
<div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
{inflight && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)} )}
</div> </div>
<div className="modal-footer-right"> <OLButton variant="secondary" onClick={cancel} disabled={inflight}>
<Button
type="button"
bsStyle={null}
className="btn-secondary"
onClick={cancel}
disabled={inflight}
>
{t('cancel')} {t('cancel')}
</Button> </OLButton>
<Button <OLButton variant="primary" onClick={confirm} disabled={inflight}>
type="button"
bsStyle="primary"
onClick={confirm}
disabled={inflight}
>
{t('change_owner')} {t('change_owner')}
</Button> </OLButton>
</div> </OLModalFooter>
</Modal.Footer> </OLModal>
</AccessibleModal>
) )
} }
TransferOwnershipModal.propTypes = { TransferOwnershipModal.propTypes = {

View file

@ -1,21 +1,27 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Col, Row } from 'react-bootstrap'
import MemberPrivileges from './member-privileges' import MemberPrivileges from './member-privileges'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export default function ViewMember({ member }) { export default function ViewMember({ member }) {
return ( return (
<Row className="project-member"> <OLRow className="project-member">
<Col xs={8}> <OLCol xs={8}>
<div className="project-member-email-icon"> <div className="project-member-email-icon">
<Icon type="user" fw /> <BootstrapVersionSwitcher
bs3={<Icon type="user" fw />}
bs5={<MaterialIcon type="person" />}
/>
<div className="email-warning">{member.email}</div> <div className="email-warning">{member.email}</div>
</div> </div>
</Col> </OLCol>
<Col xs={4} className="text-right"> <OLCol xs={4} className="text-end">
<MemberPrivileges privileges={member.privileges} /> <MemberPrivileges privileges={member.privileges} />
</Col> </OLCol>
</Row> </OLRow>
) )
} }

View file

@ -1,6 +1,12 @@
import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import OLButton from '@/features/ui/components/ol/ol-button'
import {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
type ViewOnlyAccessModalContentProps = { type ViewOnlyAccessModalContentProps = {
handleHide: () => void handleHide: () => void
@ -13,18 +19,17 @@ export default function ViewOnlyAccessModalContent({
return ( return (
<> <>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('view_only_access')}</Modal.Title> <OLModalTitle>{t('view_only_access')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<p>{t('this_project_already_has_maximum_editors')}</p> <p>{t('this_project_already_has_maximum_editors')}</p>
<p>{t('please_ask_the_project_owner_to_upgrade_more_editors')}</p> <p>{t('please_ask_the_project_owner_to_upgrade_more_editors')}</p>
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button <OLButton
bsStyle={null} variant="secondary"
className="btn-secondary"
href="/blog/changes-to-project-sharing" href="/blog/changes-to-project-sharing"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -36,9 +41,9 @@ export default function ViewOnlyAccessModalContent({
}} }}
> >
{t('learn_more')} {t('learn_more')}
</Button> </OLButton>
<Button <OLButton
className="btn-primary" variant="primary"
onClick={() => { onClick={() => {
sendMB('notification-click', { sendMB('notification-click', {
name: 'link-sharing-collaborator-limit', name: 'link-sharing-collaborator-limit',
@ -48,8 +53,8 @@ export default function ViewOnlyAccessModalContent({
}} }}
> >
{t('ok')} {t('ok')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</> </>
) )
} }

View file

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import AccessibleModal from '@/shared/components/accessible-modal'
import ViewOnlyAccessModalContent from './view-only-access-modal-content' import ViewOnlyAccessModalContent from './view-only-access-modal-content'
import customLocalStorage from '@/infrastructure/local-storage' import customLocalStorage from '@/infrastructure/local-storage'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { useEditorContext } from '@/shared/context/editor-context' import { useEditorContext } from '@/shared/context/editor-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import OLModal from '@/features/ui/components/ol/ol-modal'
const ViewOnlyAccessModal = () => { const ViewOnlyAccessModal = () => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
@ -60,7 +60,7 @@ const ViewOnlyAccessModal = () => {
]) ])
return show ? ( return show ? (
<AccessibleModal <OLModal
animation animation
show={show} show={show}
onHide={() => { onHide={() => {
@ -72,7 +72,7 @@ const ViewOnlyAccessModal = () => {
id="editor-over-limit-modal" id="editor-over-limit-modal"
> >
<ViewOnlyAccessModalContent handleHide={handleHide} /> <ViewOnlyAccessModalContent handleHide={handleHide} />
</AccessibleModal> </OLModal>
) : null ) : null
} }

View file

@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
import classnames from 'classnames' import classnames from 'classnames'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import Tag from '@/features/ui/components/bootstrap-5/tag'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
// Unicode characters in these Unicode groups: // Unicode characters in these Unicode groups:
// "General Punctuation Spaces" // "General Punctuation Spaces"
@ -154,12 +160,28 @@ export default function SelectCollaborators({
<label className="small" {...getLabelProps()}> <label className="small" {...getLabelProps()}>
{t('share_with_your_collabs')} {t('share_with_your_collabs')}
&nbsp; &nbsp;
{loading && <Icon type="refresh" spin />} {loading && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)}
</label> </label>
<div className="host"> <div className="host">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<div {...getComboboxProps()} className="tags" onClick={focusInput}> <div
{...getComboboxProps()}
className="tags form-control"
onClick={focusInput}
>
{selectedItems.map((selectedItem, index) => ( {selectedItems.map((selectedItem, index) => (
<SelectedItem <SelectedItem
key={`selected-item-${index}`} key={`selected-item-${index}`}
@ -174,8 +196,7 @@ export default function SelectCollaborators({
<input <input
{...getInputProps( {...getInputProps(
getDropdownProps({ getDropdownProps({
className: classnames({ className: classnames('input', {
input: true,
'invalid-tag': !isValidInput, 'invalid-tag': !isValidInput,
}), }),
type: 'email', type: 'email',
@ -241,8 +262,20 @@ export default function SelectCollaborators({
/> />
</div> </div>
<div className={classnames({ autocomplete: isOpen })}> <div
<ul {...getMenuProps()} className="suggestion-list"> className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
>
<ul
{...getMenuProps()}
className={classnames(
bsVersion({
bs3: 'suggestion-list',
bs5: classnames('dropdown-menu select-dropdown-menu', {
show: isOpen,
}),
})
)}
>
{isOpen && {isOpen &&
filteredOptions.map((item, index) => ( filteredOptions.map((item, index) => (
<Option <Option
@ -275,15 +308,36 @@ SelectCollaborators.propTypes = {
function Option({ selected, item, getItemProps, index }) { function Option({ selected, item, getItemProps, index }) {
return ( return (
<li <li
className={classnames('suggestion-item', { selected })} className={bsVersion({
bs3: classnames('suggestion-item', { selected }),
})}
{...getItemProps({ item, index })} {...getItemProps({ item, index })}
> >
<BootstrapVersionSwitcher
bs3={
<>
<Icon type="user" fw /> <Icon type="user" fw />
&nbsp; &nbsp;
{item.display} {item.display}
</>
}
bs5={
<DropdownItem
as="span"
role={undefined}
leadingIcon="person"
className={classnames({
active: selected,
})}
>
{item.display}
</DropdownItem>
}
/>
</li> </li>
) )
} }
Option.propTypes = { Option.propTypes = {
selected: PropTypes.bool.isRequired, selected: PropTypes.bool.isRequired,
item: PropTypes.shape({ item: PropTypes.shape({
@ -313,6 +367,8 @@ function SelectedItem({
) )
return ( return (
<BootstrapVersionSwitcher
bs3={
<span <span
className="tag-item" className="tag-item"
{...getSelectedItemProps({ selectedItem, index })} {...getSelectedItemProps({ selectedItem, index })}
@ -328,8 +384,27 @@ function SelectedItem({
<Icon type="close" fw /> <Icon type="close" fw />
</button> </button>
</span> </span>
}
bs5={
<Tag
prepend={
<BootstrapVersionSwitcher
bs3={<Icon type="user" fw />}
bs5={<MaterialIcon type="person" />}
/>
}
closeBtnProps={{
onClick: handleClick,
}}
{...getSelectedItemProps({ selectedItem, index })}
>
{selectedItem.display}
</Tag>
}
/>
) )
} }
SelectedItem.propTypes = { SelectedItem.propTypes = {
focusInput: PropTypes.func.isRequired, focusInput: PropTypes.func.isRequired,
removeSelectedItem: PropTypes.func.isRequired, removeSelectedItem: PropTypes.func.isRequired,

View file

@ -1,17 +1,18 @@
import { Col, Row } from 'react-bootstrap'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
export default function SendInvitesNotice() { export default function SendInvitesNotice() {
const { publicAccessLevel } = useProjectContext() const { publicAccessLevel } = useProjectContext()
return ( return (
<Row className="public-access-level public-access-level--notice"> <OLRow className="public-access-level public-access-level-notice">
<Col xs={12} className="text-center"> <OLCol className="text-center">
<AccessLevel level={publicAccessLevel} /> <AccessLevel level={publicAccessLevel} />
</Col> </OLCol>
</Row> </OLRow>
) )
} }

View file

@ -1,13 +1,13 @@
import { Row } from 'react-bootstrap'
import AddCollaborators from './add-collaborators' import AddCollaborators from './add-collaborators'
import AddCollaboratorsUpgrade from './add-collaborators-upgrade' import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import OLRow from '@/features/ui/components/ol/ol-row'
export default function SendInvites({ canAddCollaborators }) { export default function SendInvites({ canAddCollaborators }) {
return ( return (
<Row className="invite-controls"> <OLRow className="invite-controls">
{canAddCollaborators ? <AddCollaborators /> : <AddCollaboratorsUpgrade />} {canAddCollaborators ? <AddCollaborators /> : <AddCollaboratorsUpgrade />}
</Row> </OLRow>
) )
} }

View file

@ -1,11 +1,20 @@
import { Button, Modal, Grid } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import AccessibleModal from '../../../shared/components/accessible-modal' import { useEditorContext } from '@/shared/context/editor-context'
import { useEditorContext } from '../../../shared/context/editor-context'
import { lazy, Suspense } from 'react' import { lazy, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
const ReadOnlyTokenLink = lazy(() => const ReadOnlyTokenLink = lazy(() =>
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({ import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
const { isRestrictedTokenMember } = useEditorContext() const { isRestrictedTokenMember } = useEditorContext()
return ( return (
<AccessibleModal show={show} onHide={cancel} animation={animation}> <OLModal show={show} onHide={cancel} animation={animation}>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('share_project')}</Modal.Title> <OLModalTitle>{t('share_project')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body className="modal-body-share"> <OLModalBody className="modal-body-share">
<Grid fluid> <div className="container-fluid">
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}> <Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
{isRestrictedTokenMember ? ( {isRestrictedTokenMember ? (
<ReadOnlyTokenLink /> <ReadOnlyTokenLink />
@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
<ShareModalBody /> <ShareModalBody />
)} )}
</Suspense> </Suspense>
</Grid>
</Modal.Body>
<Modal.Footer className="modal-footer-share">
<div className="modal-footer-left">
{inFlight && <Icon type="refresh" spin />}
{error && ( {error && (
<span className="text-danger error"> <OLNotification
<ErrorMessage error={error} /> type="error"
</span> content={<ErrorMessage error={error} />}
className="mb-0 mt-3"
/>
)}
</div>
</OLModalBody>
<OLModalFooter>
<div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
{inFlight && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)} )}
</div> </div>
<div className="modal-footer-right">
<ClickableElementEnhancer <ClickableElementEnhancer
onClick={cancel} onClick={cancel}
as={Button} as={OLButton}
type="button" variant="secondary"
bsStyle={null}
className="btn-secondary"
disabled={inFlight} disabled={inFlight}
> >
{t('close')} {t('close')}
</ClickableElementEnhancer> </ClickableElementEnhancer>
</div> </OLModalFooter>
</Modal.Footer> </OLModal>
</AccessibleModal>
) )
} }

View file

@ -1,12 +1,21 @@
import { useState } from 'react' import { useState } from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { transferProjectOwnership } from '../utils/api' import { transferProjectOwnership } from '../utils/api'
import AccessibleModal from '../../../shared/components/accessible-modal'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { useLocation } from '../../../shared/hooks/use-location' import { useLocation } from '../../../shared/hooks/use-location'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import { Spinner } from 'react-bootstrap-5'
export default function TransferOwnershipModal({ member, cancel }) { export default function TransferOwnershipModal({ member, cancel }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
} }
return ( return (
<AccessibleModal show onHide={cancel}> <OLModal show onHide={cancel}>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('change_project_owner')}</Modal.Title> <OLModalTitle>{t('change_project_owner')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<p> <p>
<Trans <Trans
i18nKey="project_ownership_transfer_confirmation_1" i18nKey="project_ownership_transfer_confirmation_1"
@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
/> />
</p> </p>
<p>{t('project_ownership_transfer_confirmation_2')}</p> <p>{t('project_ownership_transfer_confirmation_2')}</p>
</Modal.Body>
<Modal.Footer>
<div className="modal-footer-left">
{inflight && <Icon type="refresh" spin />}
{error && ( {error && (
<span className="text-danger"> <OLNotification
{t('generic_something_went_wrong')} type="error"
</span> content={t('generic_something_went_wrong')}
className="mb-0 mt-3"
/>
)}
</OLModalBody>
<OLModalFooter>
<div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
{inflight && (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)} )}
</div> </div>
<div className="modal-footer-right"> <OLButton variant="secondary" onClick={cancel} disabled={inflight}>
<Button
type="button"
bsStyle={null}
className="btn-secondary"
onClick={cancel}
disabled={inflight}
>
{t('cancel')} {t('cancel')}
</Button> </OLButton>
<Button <OLButton variant="primary" onClick={confirm} disabled={inflight}>
type="button"
bsStyle="primary"
onClick={confirm}
disabled={inflight}
>
{t('change_owner')} {t('change_owner')}
</Button> </OLButton>
</div> </OLModalFooter>
</Modal.Footer> </OLModal>
</AccessibleModal>
) )
} }
TransferOwnershipModal.propTypes = { TransferOwnershipModal.propTypes = {

View file

@ -1,15 +1,16 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Col, Row } from 'react-bootstrap'
import MemberPrivileges from './member-privileges' import MemberPrivileges from './member-privileges'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
export default function ViewMember({ member }) { export default function ViewMember({ member }) {
return ( return (
<Row className="project-member"> <OLRow className="project-member">
<Col xs={7}>{member.email}</Col> <OLCol xs={7}>{member.email}</OLCol>
<Col xs={3}> <OLCol xs={3}>
<MemberPrivileges privileges={member.privileges} /> <MemberPrivileges privileges={member.privileges} />
</Col> </OLCol>
</Row> </OLRow>
) )
} }

View file

@ -6,11 +6,15 @@ import type { IconButtonProps } from '@/features/ui/components/types/icon-button
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>( const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
( (
{ accessibilityLabel, icon, isLoading = false, size = 'default', ...props }, { accessibilityLabel, icon, isLoading = false, size, className, ...props },
ref ref
) => { ) => {
const iconButtonClassName = `icon-button-${size}` const iconButtonClassName = classNames(className, {
const iconSizeClassName = size === 'large' ? 'icon-large' : 'icon-small' 'icon-button': !size,
'icon-button-small': size === 'sm',
'icon-button-large': size === 'lg',
})
const iconSizeClassName = size === 'lg' ? 'icon-large' : 'icon-small'
const materialIconClassName = classNames(iconSizeClassName, { const materialIconClassName = classNames(iconSizeClassName, {
'button-content-hidden': isLoading, 'button-content-hidden': isLoading,
}) })

View file

@ -38,6 +38,7 @@ export function bs3ButtonProps(props: ButtonProps) {
target: props.target, target: props.target,
rel: props.rel, rel: props.rel,
onClick: props.onClick, onClick: props.onClick,
onMouseDown: props.onMouseDown as BS3ButtonProps['onMouseDown'],
type: props.type, type: props.type,
draggable: props.draggable, draggable: props.draggable,
download: props.download, download: props.download,

View file

@ -11,8 +11,8 @@ function OLCol(props: OLColProps) {
const getBs3Sizes = (obj: typeof rest) => { const getBs3Sizes = (obj: typeof rest) => {
const bs5ToBs3SizeMap = { const bs5ToBs3SizeMap = {
xs: undefined, xs: 'xs',
sm: undefined, sm: 'xs',
md: 'sm', md: 'sm',
lg: 'md', lg: 'md',
xl: 'lg', xl: 'lg',

View file

@ -14,6 +14,7 @@ export type ButtonProps = {
isLoading?: boolean isLoading?: boolean
loadingLabel?: string loadingLabel?: string
onClick?: React.MouseEventHandler<HTMLButtonElement> onClick?: React.MouseEventHandler<HTMLButtonElement>
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>
onMouseOver?: React.MouseEventHandler<HTMLButtonElement> onMouseOver?: React.MouseEventHandler<HTMLButtonElement>
onMouseOut?: React.MouseEventHandler<HTMLButtonElement> onMouseOut?: React.MouseEventHandler<HTMLButtonElement>
onFocus?: React.FocusEventHandler<HTMLButtonElement> onFocus?: React.FocusEventHandler<HTMLButtonElement>

View file

@ -73,6 +73,7 @@ const IconButton: FC<{
<OLIconButton <OLIconButton
onClick={handleClick} onClick={handleClick}
variant="link" variant="link"
size="sm"
accessibilityLabel={t('copy')} accessibilityLabel={t('copy')}
className="copy-button" className="copy-button"
bs3Props={{ bsSize: 'xsmall' }} bs3Props={{ bsSize: 'xsmall' }}

View file

@ -62,7 +62,7 @@
} }
} }
.public-access-level.public-access-level--notice { .public-access-level.public-access-level-notice {
background-color: @gray-lightest; background-color: @gray-lightest;
border-bottom: none; border-bottom: none;
margin-top: @margin-md; margin-top: @margin-md;
@ -167,12 +167,6 @@
} }
} }
} }
.modal-footer-share {
.modal-footer-left {
max-width: 70%;
text-align: left;
}
}
.copy-button:focus-within { .copy-button:focus-within {
outline: none; outline: none;

View file

@ -39,3 +39,9 @@
animation-name: bounce; animation-name: bounce;
transform-origin: center bottom; transform-origin: center bottom;
} }
.recaptcha-branding {
padding: var(--spacing-05) var(--spacing-05) 0 var(--spacing-05);
text-align: center;
font-size: $font-size-sm;
}

View file

@ -26,3 +26,4 @@
@import 'select'; @import 'select';
@import 'link'; @import 'link';
@import 'pagination'; @import 'pagination';
@import 'loading-spinner';

View file

@ -203,7 +203,7 @@
padding: var(--spacing-01); padding: var(--spacing-01);
} }
.icon-button-default { .icon-button {
padding: var(--spacing-04); padding: var(--spacing-04);
} }

View file

@ -184,3 +184,8 @@
margin-left: var(--spacing-04); margin-left: var(--spacing-04);
} }
} }
%input-focus-style {
border-color: $input-focus-border-color;
box-shadow: $form-check-input-focus-box-shadow;
}

View file

@ -0,0 +1,7 @@
.full-size-loading-spinner-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -15,5 +15,7 @@
@import 'subscription'; @import 'subscription';
@import 'editor/pdf'; @import 'editor/pdf';
@import 'editor/compile-button'; @import 'editor/compile-button';
@import 'editor/share';
@import 'editor/tags-input';
@import 'website-redesign'; @import 'website-redesign';
@import 'group-settings'; @import 'group-settings';

View file

@ -0,0 +1,188 @@
.modal-body-share {
h3 {
border-bottom: 1px solid var(--neutral-30);
padding-bottom: calc(var(--line-height-03) / 4);
margin: 0;
font-size: var(--font-size-03);
}
.project-member.form-group {
margin-bottom: 0;
}
.project-member .remove-button {
font-size: inherit;
text-decoration: none;
}
.project-invite,
.public-access-level {
font-size: var(--font-size-02);
padding: calc(var(--line-height-03) / 2) 0;
border-bottom: 1px solid var(--neutral-30);
}
.public-access-level {
margin-top: calc(var(--line-height-03) / 4);
font-size: var(--font-size-02);
padding-bottom: var(--spacing-07);
.access-token-display-area {
margin-top: calc(var(--line-height-03) / 4);
.access-token-wrapper {
padding-top: calc(var(--line-height-03) / 4);
.access-token {
margin-top: calc(var(--line-height-03) / 4);
background-color: var(--neutral-10);
border: 1px solid var(--neutral-30);
padding: var(--spacing-03) var(--spacing-05);
display: flex;
align-items: center;
justify-content: space-between;
code {
font-size: inherit;
}
}
}
}
.btn-chevron {
padding: 0 calc(var(--line-height-03) / 2);
text-decoration: none;
color: var(--neutral-70);
}
}
.public-access-level.public-access-level-notice {
background-color: var(--neutral-10);
border-bottom: none;
margin-top: var(--spacing-07);
padding-top: var(--spacing-07);
}
.project-member,
.project-invite {
&:hover {
background-color: var(--neutral-10);
}
}
.project-member {
padding: calc(var(--line-height-03) / 2) 0;
font-size: var(--font-size-03);
.select-wrapper {
width: 100px;
margin-left: auto;
.select-trigger {
padding: 0 var(--spacing-05);
color: var(--neutral-70);
border: none;
background-color: transparent;
&:focus {
box-shadow: none;
}
}
.dropdown-menu {
li:last-child .dropdown-item {
color: var(--bs-danger);
}
}
}
.project-member-email-icon {
display: grid;
grid-template-columns: 2em auto;
align-items: center;
padding-bottom: var(--spacing-03);
.subtitle {
font-size: $font-size-sm;
}
}
}
.project-member .text-left,
.project-invite .text-left {
padding-left: var(--spacing-08);
}
.invite-controls {
.small {
padding: var(--spacing-01);
margin-bottom: 0;
}
padding: calc(var(--line-height-03) / 2);
background-color: var(--neutral-10);
margin-top: calc(var(--line-height-03) / 2);
form {
.form-group {
margin-bottom: calc(var(--line-height-03) / 2);
&:last-child {
margin-bottom: 0;
}
}
.privileges {
display: inline-block;
width: auto;
}
.tags-new .privileges {
background: transparent;
width: auto;
height: 30px;
font-size: var(--font-size-02);
border: none;
border-right: 5px solid transparent;
}
}
.add-collaborators-upgrade {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: var(--spacing-08);
.upgrade-actions {
display: flex;
gap: var(--spacing-07);
}
}
}
}
.copy-button:focus-within {
outline: none;
}
.modal-link-share-new {
.invite-controls {
padding: 0;
background-color: transparent;
margin-top: 0;
}
.public-access-level {
border: none;
}
.invite-warning {
margin-bottom: calc(var(--line-height-03) / 2);
}
.project-member-select {
display: flex;
align-items: center;
padding: 0;
}
}

View file

@ -0,0 +1,68 @@
.tags-input *,
.tags-input *::before,
.tags-input *::after {
box-sizing: border-box;
}
.tags-input label.small {
font-weight: normal;
}
.tags-input .host {
position: relative;
height: 100%;
}
.tags-input .host:active,
.tags-input[disabled] .host:focus {
outline: none;
}
.tags-input .tags {
appearance: textfield;
overflow: hidden;
word-wrap: break-word;
cursor: text;
background-color: var(--white);
height: 100%;
display: flex;
flex-wrap: wrap;
.badge-tag {
margin: var(--spacing-01);
.badge-tag-content {
max-width: initial;
.badge-content {
white-space: initial;
word-break: break-word;
text-align: initial;
}
}
}
}
.tags-input .tags:focus-within {
@extend %input-focus-style;
}
.tags-input .tags .input {
border: 0;
outline: none;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
&::placeholder {
color: $input-placeholder-color;
}
}
.tags-input .tags .input.invalid-tag {
color: var(--bs-danger);
}
.tags-input .tags .input::-ms-clear {
display: none;
}