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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,18 @@
import { useProjectContext } from '../../../shared/context/project-context'
import { Col, Row } from 'react-bootstrap'
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() {
const { t } = useTranslation()
const { owner } = useProjectContext()
return (
<Row className="project-member">
<Col xs={7}>{owner?.email}</Col>
<Col xs={3} className="text-left">
<OLRow className="project-member">
<OLCol xs={7}>{owner?.email}</OLCol>
<OLCol xs={3} className="text-start">
{t('owner')}
</Col>
</Row>
</OLCol>
</OLRow>
)
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Notification from '@/shared/components/notification'
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 StartFreeTrialButton from '@/shared/components/start-free-trial-button'
import getMeta from '@/utils/meta'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function CollaboratorsLimitUpgrade() {
const { t } = useTranslation()
@ -44,15 +44,14 @@ export default function CollaboratorsLimitUpgrade() {
{t('upgrade')}
</StartFreeTrialButton>
) : (
<Button
bsSize="medium"
className="btn-premium"
<OLButton
variant="premium"
onClick={() => {
upgradePlan('project-sharing')
}}
>
{t('upgrade')}
</Button>
</OLButton>
)
}
/>
@ -78,15 +77,15 @@ export default function CollaboratorsLimitUpgrade() {
{t('upgrade')}
</StartFreeTrialButton>
) : (
<Button
bsSize="sm"
className="btn-secondary"
<OLButton
size="sm"
variant="secondary"
onClick={() => {
upgradePlan('project-sharing')
}}
>
{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 { useTranslation } from 'react-i18next'
import { useShareProjectContext } from './share-project-modal'
import TransferOwnershipModal from './transfer-ownership-modal'
import { removeMemberFromProject, updateMember } from '../../utils/api'
import { Button, Col, Form, FormGroup } from 'react-bootstrap'
import Icon from '@/shared/components/icon'
import { useProjectContext } from '@/shared/context/project-context'
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 { PermissionsLevel } from '@/features/ide-react/types/permissions'
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'
@ -115,22 +121,44 @@ export default function EditMember({
}
return (
<Form
horizontal
<form
className={bsVersion({ bs3: 'form-horizontal' })}
id="share-project-form"
onSubmit={e => {
e.preventDefault()
commitPrivilegeChange(privileges)
}}
>
<FormGroup className="project-member">
<Col xs={7}>
<OLFormGroup
className={classnames('project-member', bsVersion({ bs5: 'row' }))}
>
<OLCol xs={7}>
<div className="project-member-email-icon">
<Icon
type={
shouldWarnMember() || member.pendingEditor ? 'warning' : 'user'
<BootstrapVersionSwitcher
bs3={
<Icon
type={
shouldWarnMember() || member.pendingEditor
? 'warning'
: 'user'
}
fw
/>
}
bs5={
<MaterialIcon
type={
shouldWarnMember() || member.pendingEditor
? 'warning'
: 'person'
}
className={
shouldWarnMember() || member.pendingEditor
? 'text-warning'
: undefined
}
/>
}
fw
/>
<div className="email-warning">
{member.email}
@ -146,18 +174,23 @@ export default function EditMember({
)}
</div>
</div>
</Col>
</OLCol>
<Col xs={1}>
<OLCol xs={2}>
{privileges !== member.privileges && privilegeChangePending && (
<ChangePrivilegesActions
handleReset={() => setPrivileges(member.privileges)}
/>
)}
</Col>
</OLCol>
<Col xs={4} className="project-member-select">
{hasBeenDowngraded && <Icon type="warning" fw />}
<OLCol xs={3} className="project-member-select">
{hasBeenDowngraded && (
<BootstrapVersionSwitcher
bs3={<Icon type="warning" fw />}
bs5={<MaterialIcon type="warning" className="text-warning" />}
/>
)}
<SelectPrivilege
value={privileges}
@ -169,9 +202,9 @@ export default function EditMember({
hasBeenDowngraded={hasBeenDowngraded}
canAddCollaborators={canAddCollaborators}
/>
</Col>
</FormGroup>
</Form>
</OLCol>
</OLFormGroup>
</form>
)
}
EditMember.propTypes = {
@ -262,8 +295,9 @@ function SelectPrivilege({
}
type ChangePrivilegesActionsProps = {
handleReset: MouseEventHandler<Button>
handleReset: React.ComponentProps<typeof OLButton>['onClick']
}
function ChangePrivilegesActions({
handleReset,
}: ChangePrivilegesActionsProps) {
@ -271,15 +305,19 @@ function ChangePrivilegesActions({
return (
<div className="text-center">
<Button type="submit" bsSize="sm" bsStyle="primary">
<OLButton type="submit" size="sm" variant="primary">
{t('change_or_cancel-change')}
</Button>
</OLButton>
<div className="text-sm">
{t('change_or_cancel-or')}
&nbsp;
<Button type="button" className="btn-inline-link" onClick={handleReset}>
<OLButton
variant="link"
className="btn-inline-link"
onClick={handleReset}
>
{t('change_or_cancel-cancel')}
</Button>
</OLButton>
</div>
</div>
)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,29 @@
import { useProjectContext } from '@/shared/context/project-context'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
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() {
const { t } = useTranslation()
const { owner } = useProjectContext()
return (
<Row className="project-member">
<Col xs={8}>
<OLRow className="project-member">
<OLCol xs={8}>
<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>
</Col>
<Col xs={4} className="text-right">
</OLCol>
<OLCol xs={4} className="text-end">
{t('owner')}
</Col>
</Row>
</OLCol>
</OLRow>
)
}

View file

@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
import classnames from 'classnames'
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:
// "General Punctuation Spaces"
@ -156,98 +162,123 @@ export default function SelectCollaborators({
{t('add_people')}
&nbsp;
</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>
<div className="host">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<div {...getComboboxProps()} className="tags" onClick={focusInput}>
<div className="tags-main">
{selectedItems.map((selectedItem, index) => (
<SelectedItem
key={`selected-item-${index}`}
removeSelectedItem={removeSelectedItem}
selectedItem={selectedItem}
focusInput={focusInput}
index={index}
getSelectedItemProps={getSelectedItemProps}
/>
))}
<div
{...getComboboxProps()}
className="tags form-control"
onClick={focusInput}
>
{selectedItems.map((selectedItem, index) => (
<SelectedItem
key={`selected-item-${index}`}
removeSelectedItem={removeSelectedItem}
selectedItem={selectedItem}
focusInput={focusInput}
index={index}
getSelectedItemProps={getSelectedItemProps}
/>
))}
<input
{...getInputProps(
getDropdownProps({
className: classnames({
input: true,
'invalid-tag': !isValidInput,
}),
type: 'email',
placeholder,
size: inputValue.length
? inputValue.length + 5
: placeholder.length,
ref: inputRef,
// preventKeyAction: showDropdown,
onBlur: () => {
addNewItem(inputValue, false)
},
onChange: e => {
setInputValue(e.target.value)
},
onClick: () => focusInput,
onKeyDown: event => {
switch (event.key) {
case 'Enter':
// Enter: always prevent form submission
<input
{...getInputProps(
getDropdownProps({
className: classnames('input', {
'invalid-tag': !isValidInput,
}),
type: 'email',
placeholder,
size: inputValue.length
? inputValue.length + 5
: placeholder.length,
ref: inputRef,
// preventKeyAction: showDropdown,
onBlur: () => {
addNewItem(inputValue, false)
},
onChange: e => {
setInputValue(e.target.value)
},
onClick: () => focusInput,
onKeyDown: event => {
switch (event.key) {
case 'Enter':
// Enter: always prevent form submission
event.preventDefault()
event.stopPropagation()
break
case 'Tab':
// Tab: if the dropdown isn't open, try to create a new item using inputValue and prevent blur if successful
if (!isOpen && addNewItem(inputValue)) {
event.preventDefault()
event.stopPropagation()
break
}
break
case 'Tab':
// Tab: if the dropdown isn't open, try to create a new item using inputValue and prevent blur if successful
if (!isOpen && addNewItem(inputValue)) {
event.preventDefault()
event.stopPropagation()
}
break
case ',':
// comma: try to create a new item using inputValue
event.preventDefault()
addNewItem(inputValue)
break
}
},
onPaste: event => {
const data =
// modern browsers
event.clipboardData?.getData('text/plain') ??
// IE11
window.clipboardData?.getData('text')
case ',':
// comma: try to create a new item using inputValue
event.preventDefault()
addNewItem(inputValue)
break
}
},
onPaste: event => {
const data =
// modern browsers
event.clipboardData?.getData('text/plain') ??
// IE11
window.clipboardData?.getData('text')
if (data) {
const emails = data
.split(/[\r\n,; ]+/)
.filter(item => item.includes('@'))
if (data) {
const emails = data
.split(/[\r\n,; ]+/)
.filter(item => item.includes('@'))
if (emails.length) {
// pasted comma-separated email addresses
event.preventDefault()
if (emails.length) {
// pasted comma-separated email addresses
event.preventDefault()
for (const email of emails) {
addNewItem(email)
}
for (const email of emails) {
addNewItem(email)
}
}
},
})
)}
/>
</div>
}
},
})
)}
/>
</div>
<div className={classnames({ autocomplete: isOpen })}>
<ul {...getMenuProps()} className="suggestion-list">
<div
className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
>
<ul
{...getMenuProps()}
className={classnames(
bsVersion({
bs3: 'suggestion-list',
bs5: classnames('dropdown-menu select-dropdown-menu', {
show: isOpen,
}),
})
)}
>
{isOpen &&
filteredOptions.map((item, index) => (
<Option
@ -280,15 +311,36 @@ SelectCollaborators.propTypes = {
function Option({ selected, item, getItemProps, index }) {
return (
<li
className={classnames('suggestion-item', { selected })}
className={bsVersion({
bs3: classnames('suggestion-item', { selected }),
})}
{...getItemProps({ item, index })}
>
<Icon type="user" fw />
&nbsp;
{item.display}
<BootstrapVersionSwitcher
bs3={
<>
<Icon type="user" fw />
&nbsp;
{item.display}
</>
}
bs5={
<DropdownItem
as="span"
role={undefined}
leadingIcon="person"
className={classnames({
active: selected,
})}
>
{item.display}
</DropdownItem>
}
/>
</li>
)
}
Option.propTypes = {
selected: PropTypes.bool.isRequired,
item: PropTypes.shape({
@ -318,23 +370,44 @@ function SelectedItem({
)
return (
<span
className="tag-item"
{...getSelectedItemProps({ selectedItem, index })}
>
<Icon type="user" fw />
<span>{selectedItem.display}</span>
<button
type="button"
className="remove-button btn-inline-link"
aria-label={t('remove')}
onClick={handleClick}
>
<Icon type="close" fw />
</button>
</span>
<BootstrapVersionSwitcher
bs3={
<span
className="tag-item"
{...getSelectedItemProps({ selectedItem, index })}
>
<Icon type="user" fw />
<span>{selectedItem.display}</span>
<button
type="button"
className="remove-button btn-inline-link"
aria-label={t('remove')}
onClick={handleClick}
>
<Icon type="close" fw />
</button>
</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 = {
focusInput: 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 { useProjectContext } from '@/shared/context/project-context'
import Notification from '@/shared/components/notification'
import { PublicAccessLevel } from '../../../../../../types/public-access-level'
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() {
const { publicAccessLevel } = useProjectContext()
@ -25,11 +26,11 @@ export default function SendInvitesNotice() {
}
/>
)}
<Row className="public-access-level public-access-level--notice">
<Col xs={12} className="text-center">
<OLRow className="public-access-level public-access-level-notice">
<OLCol className="text-center">
<AccessLevel level={publicAccessLevel} />
</Col>
</Row>
</OLCol>
</OLRow>
</div>
)
}

View file

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

View file

@ -1,11 +1,20 @@
import { Button, Modal, Grid } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Icon from '@/shared/components/icon'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useEditorContext } from '@/shared/context/editor-context'
import { lazy, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
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(() =>
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
const { isRestrictedTokenMember } = useEditorContext()
return (
<AccessibleModal show={show} onHide={cancel} animation={animation}>
<Modal.Header closeButton>
<Modal.Title>{t('share_project')}</Modal.Title>
</Modal.Header>
<OLModal show={show} onHide={cancel} animation={animation}>
<OLModalHeader closeButton>
<OLModalTitle>{t('share_project')}</OLModalTitle>
</OLModalHeader>
<Modal.Body className="modal-body-share modal-link-share-new">
<Grid fluid>
<OLModalBody className="modal-body-share modal-link-share-new">
<div className="container-fluid">
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
{isRestrictedTokenMember ? (
<ReadOnlyTokenLink />
@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
<ShareModalBody />
)}
</Suspense>
</Grid>
</Modal.Body>
<Modal.Footer className="modal-footer-share">
<div className="modal-footer-left">
{inFlight && <Icon type="refresh" spin />}
{error && (
<span className="text-danger error">
<ErrorMessage error={error} />
</span>
<OLNotification
type="error"
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 className="modal-footer-right">
<ClickableElementEnhancer
onClick={cancel}
as={Button}
type="button"
bsStyle={null}
className="btn-secondary"
disabled={inFlight}
>
{t('close')}
</ClickableElementEnhancer>
</div>
</Modal.Footer>
</AccessibleModal>
<ClickableElementEnhancer
onClick={cancel}
as={OLButton}
variant="secondary"
disabled={inFlight}
>
{t('close')}
</ClickableElementEnhancer>
</OLModalFooter>
</OLModal>
)
}

View file

@ -1,12 +1,21 @@
import { useState } from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import Icon from '@/shared/components/icon'
import { transferProjectOwnership } from '../../utils/api'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useProjectContext } from '@/shared/context/project-context'
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 }) {
const { t } = useTranslation()
@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
}
return (
<AccessibleModal show onHide={cancel}>
<Modal.Header closeButton>
<Modal.Title>{t('change_project_owner')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<OLModal show onHide={cancel}>
<OLModalHeader closeButton>
<OLModalTitle>{t('change_project_owner')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>
<Trans
i18nKey="project_ownership_transfer_confirmation_1"
@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
/>
</p>
<p>{t('project_ownership_transfer_confirmation_2')}</p>
</Modal.Body>
<Modal.Footer>
<div className="modal-footer-left">
{inflight && <Icon type="refresh" spin />}
{error && (
<span className="text-danger">
{t('generic_something_went_wrong')}
</span>
{error && (
<OLNotification
type="error"
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 className="modal-footer-right">
<Button
type="button"
bsStyle={null}
className="btn-secondary"
onClick={cancel}
disabled={inflight}
>
{t('cancel')}
</Button>
<Button
type="button"
bsStyle="primary"
onClick={confirm}
disabled={inflight}
>
{t('change_owner')}
</Button>
</div>
</Modal.Footer>
</AccessibleModal>
<OLButton variant="secondary" onClick={cancel} disabled={inflight}>
{t('cancel')}
</OLButton>
<OLButton variant="primary" onClick={confirm} disabled={inflight}>
{t('change_owner')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
TransferOwnershipModal.propTypes = {

View file

@ -1,21 +1,27 @@
import PropTypes from 'prop-types'
import { Col, Row } from 'react-bootstrap'
import MemberPrivileges from './member-privileges'
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 }) {
return (
<Row className="project-member">
<Col xs={8}>
<OLRow className="project-member">
<OLCol xs={8}>
<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>
</Col>
<Col xs={4} className="text-right">
</OLCol>
<OLCol xs={4} className="text-end">
<MemberPrivileges privileges={member.privileges} />
</Col>
</Row>
</OLCol>
</OLRow>
)
}

View file

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

View file

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

View file

@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
import classnames from 'classnames'
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:
// "General Punctuation Spaces"
@ -154,12 +160,28 @@ export default function SelectCollaborators({
<label className="small" {...getLabelProps()}>
{t('share_with_your_collabs')}
&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>
<div className="host">
{/* 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) => (
<SelectedItem
key={`selected-item-${index}`}
@ -174,8 +196,7 @@ export default function SelectCollaborators({
<input
{...getInputProps(
getDropdownProps({
className: classnames({
input: true,
className: classnames('input', {
'invalid-tag': !isValidInput,
}),
type: 'email',
@ -241,8 +262,20 @@ export default function SelectCollaborators({
/>
</div>
<div className={classnames({ autocomplete: isOpen })}>
<ul {...getMenuProps()} className="suggestion-list">
<div
className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
>
<ul
{...getMenuProps()}
className={classnames(
bsVersion({
bs3: 'suggestion-list',
bs5: classnames('dropdown-menu select-dropdown-menu', {
show: isOpen,
}),
})
)}
>
{isOpen &&
filteredOptions.map((item, index) => (
<Option
@ -275,15 +308,36 @@ SelectCollaborators.propTypes = {
function Option({ selected, item, getItemProps, index }) {
return (
<li
className={classnames('suggestion-item', { selected })}
className={bsVersion({
bs3: classnames('suggestion-item', { selected }),
})}
{...getItemProps({ item, index })}
>
<Icon type="user" fw />
&nbsp;
{item.display}
<BootstrapVersionSwitcher
bs3={
<>
<Icon type="user" fw />
&nbsp;
{item.display}
</>
}
bs5={
<DropdownItem
as="span"
role={undefined}
leadingIcon="person"
className={classnames({
active: selected,
})}
>
{item.display}
</DropdownItem>
}
/>
</li>
)
}
Option.propTypes = {
selected: PropTypes.bool.isRequired,
item: PropTypes.shape({
@ -313,23 +367,44 @@ function SelectedItem({
)
return (
<span
className="tag-item"
{...getSelectedItemProps({ selectedItem, index })}
>
<Icon type="user" fw />
<span>{selectedItem.display}</span>
<button
type="button"
className="remove-button btn-inline-link"
aria-label={t('remove')}
onClick={handleClick}
>
<Icon type="close" fw />
</button>
</span>
<BootstrapVersionSwitcher
bs3={
<span
className="tag-item"
{...getSelectedItemProps({ selectedItem, index })}
>
<Icon type="user" fw />
<span>{selectedItem.display}</span>
<button
type="button"
className="remove-button btn-inline-link"
aria-label={t('remove')}
onClick={handleClick}
>
<Icon type="close" fw />
</button>
</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 = {
focusInput: 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 { useTranslation } from 'react-i18next'
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() {
const { publicAccessLevel } = useProjectContext()
return (
<Row className="public-access-level public-access-level--notice">
<Col xs={12} className="text-center">
<OLRow className="public-access-level public-access-level-notice">
<OLCol className="text-center">
<AccessLevel level={publicAccessLevel} />
</Col>
</Row>
</OLCol>
</OLRow>
)
}

View file

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

View file

@ -1,11 +1,20 @@
import { Button, Modal, Grid } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
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 { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
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(() =>
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
const { isRestrictedTokenMember } = useEditorContext()
return (
<AccessibleModal show={show} onHide={cancel} animation={animation}>
<Modal.Header closeButton>
<Modal.Title>{t('share_project')}</Modal.Title>
</Modal.Header>
<OLModal show={show} onHide={cancel} animation={animation}>
<OLModalHeader closeButton>
<OLModalTitle>{t('share_project')}</OLModalTitle>
</OLModalHeader>
<Modal.Body className="modal-body-share">
<Grid fluid>
<OLModalBody className="modal-body-share">
<div className="container-fluid">
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
{isRestrictedTokenMember ? (
<ReadOnlyTokenLink />
@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
<ShareModalBody />
)}
</Suspense>
</Grid>
</Modal.Body>
<Modal.Footer className="modal-footer-share">
<div className="modal-footer-left">
{inFlight && <Icon type="refresh" spin />}
{error && (
<span className="text-danger error">
<ErrorMessage error={error} />
</span>
<OLNotification
type="error"
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 className="modal-footer-right">
<ClickableElementEnhancer
onClick={cancel}
as={Button}
type="button"
bsStyle={null}
className="btn-secondary"
disabled={inFlight}
>
{t('close')}
</ClickableElementEnhancer>
</div>
</Modal.Footer>
</AccessibleModal>
<ClickableElementEnhancer
onClick={cancel}
as={OLButton}
variant="secondary"
disabled={inFlight}
>
{t('close')}
</ClickableElementEnhancer>
</OLModalFooter>
</OLModal>
)
}

View file

@ -1,12 +1,21 @@
import { useState } from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import Icon from '../../../shared/components/icon'
import { transferProjectOwnership } from '../utils/api'
import AccessibleModal from '../../../shared/components/accessible-modal'
import { useProjectContext } from '../../../shared/context/project-context'
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 }) {
const { t } = useTranslation()
@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
}
return (
<AccessibleModal show onHide={cancel}>
<Modal.Header closeButton>
<Modal.Title>{t('change_project_owner')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<OLModal show onHide={cancel}>
<OLModalHeader closeButton>
<OLModalTitle>{t('change_project_owner')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>
<Trans
i18nKey="project_ownership_transfer_confirmation_1"
@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
/>
</p>
<p>{t('project_ownership_transfer_confirmation_2')}</p>
</Modal.Body>
<Modal.Footer>
<div className="modal-footer-left">
{inflight && <Icon type="refresh" spin />}
{error && (
<span className="text-danger">
{t('generic_something_went_wrong')}
</span>
{error && (
<OLNotification
type="error"
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 className="modal-footer-right">
<Button
type="button"
bsStyle={null}
className="btn-secondary"
onClick={cancel}
disabled={inflight}
>
{t('cancel')}
</Button>
<Button
type="button"
bsStyle="primary"
onClick={confirm}
disabled={inflight}
>
{t('change_owner')}
</Button>
</div>
</Modal.Footer>
</AccessibleModal>
<OLButton variant="secondary" onClick={cancel} disabled={inflight}>
{t('cancel')}
</OLButton>
<OLButton variant="primary" onClick={confirm} disabled={inflight}>
{t('change_owner')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
TransferOwnershipModal.propTypes = {

View file

@ -1,15 +1,16 @@
import PropTypes from 'prop-types'
import { Col, Row } from 'react-bootstrap'
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 }) {
return (
<Row className="project-member">
<Col xs={7}>{member.email}</Col>
<Col xs={3}>
<OLRow className="project-member">
<OLCol xs={7}>{member.email}</OLCol>
<OLCol xs={3}>
<MemberPrivileges privileges={member.privileges} />
</Col>
</Row>
</OLCol>
</OLRow>
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -73,6 +73,7 @@ const IconButton: FC<{
<OLIconButton
onClick={handleClick}
variant="link"
size="sm"
accessibilityLabel={t('copy')}
className="copy-button"
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;
border-bottom: none;
margin-top: @margin-md;
@ -167,12 +167,6 @@
}
}
}
.modal-footer-share {
.modal-footer-left {
max-width: 70%;
text-align: left;
}
}
.copy-button:focus-within {
outline: none;

View file

@ -39,3 +39,9 @@
animation-name: bounce;
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 'link';
@import 'pagination';
@import 'loading-spinner';

View file

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

View file

@ -184,3 +184,8 @@
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 'editor/pdf';
@import 'editor/compile-button';
@import 'editor/share';
@import 'editor/tags-input';
@import 'website-redesign';
@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;
}