Merge pull request #32924 from overleaf/ii-share-modal-basic-layout
[web] Share modal redesign base layout GitOrigin-RevId: 0f011d03762c6669a0fd3b1fc2af32c9026c7ea4
This commit is contained in:
@@ -481,6 +481,7 @@ const _ProjectController = {
|
||||
'editor-tabs',
|
||||
'overleaf-code',
|
||||
'export-docx',
|
||||
'sharing-updates',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"access_denied": "",
|
||||
"access_edit_your_projects": "",
|
||||
"access_levels_changed": "",
|
||||
"access_updated": "",
|
||||
"account_associated_with_x": "",
|
||||
"account_billed_manually": "",
|
||||
"account_has_been_link_to_group_account": "",
|
||||
@@ -150,8 +151,10 @@
|
||||
"anonymous": "",
|
||||
"anyone": "",
|
||||
"anyone_in_x": "",
|
||||
"anyone_in_x_with_the_link": "",
|
||||
"anyone_with_link_can_edit": "",
|
||||
"anyone_with_link_can_view": "",
|
||||
"anyone_with_the_link": "",
|
||||
"app_on_x": "",
|
||||
"appearance": "",
|
||||
"apply": "",
|
||||
@@ -1097,6 +1100,7 @@
|
||||
"make_owner": "",
|
||||
"make_primary": "",
|
||||
"make_private": "",
|
||||
"manage_access": "",
|
||||
"manage_beta_program_membership": "",
|
||||
"manage_email_preferences": "",
|
||||
"manage_files_from_your_dropbox_folder": "",
|
||||
@@ -1245,6 +1249,7 @@
|
||||
"not_captured": "",
|
||||
"not_managed": "",
|
||||
"not_now": "",
|
||||
"not_permitted_by_your_organization": "",
|
||||
"notification": "",
|
||||
"notification_personal_and_group_subscriptions": "",
|
||||
"notification_project_invite_accepted_message": "",
|
||||
@@ -1268,6 +1273,7 @@
|
||||
"only_group_admin_or_managers_can_delete_your_account_7": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_9": "",
|
||||
"only_importer_can_refresh": "",
|
||||
"only_invited_people": "",
|
||||
"only_project_owner_can_link_github": "",
|
||||
"open_action_menu": "",
|
||||
"open_advanced_reference_search": "",
|
||||
@@ -1426,6 +1432,7 @@
|
||||
"processing_uppercase": "",
|
||||
"professional": "",
|
||||
"progress_bar_percentage": "",
|
||||
"project_access": "",
|
||||
"project_actions": "",
|
||||
"project_approaching_file_limit": "",
|
||||
"project_categories_tags": "",
|
||||
@@ -1539,6 +1546,8 @@
|
||||
"remove_link": "",
|
||||
"remove_manager": "",
|
||||
"remove_or_replace_figure": "",
|
||||
"remove_original_links": "",
|
||||
"remove_original_sharing_links": "",
|
||||
"remove_secondary_email_addresses": "",
|
||||
"remove_secondary_email_addresses_except_saml_email": "",
|
||||
"remove_sso_login_option": "",
|
||||
@@ -1722,6 +1731,7 @@
|
||||
"share": "",
|
||||
"share_feedback": "",
|
||||
"share_project": "",
|
||||
"share_project_name": "",
|
||||
"shared_with_you": "",
|
||||
"sharelatex_beta_program": "",
|
||||
"sharing_permissions": "",
|
||||
@@ -1970,6 +1980,8 @@
|
||||
"this_action_cannot_be_undone": "",
|
||||
"this_action_will_also_disable_domain_capture": "",
|
||||
"this_address_will_be_shown_on_the_invoice": "",
|
||||
"this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link": "",
|
||||
"this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link": "",
|
||||
"this_could_be_because_we_cant_support_some_elements_of_the_table": "",
|
||||
"this_field_is_required": "",
|
||||
"this_grants_access_to_features_2": "",
|
||||
@@ -2233,6 +2245,7 @@
|
||||
"verify_email_address_before_enabling_managed_users": "",
|
||||
"verify_your_email_address": "",
|
||||
"verifying_domain": "",
|
||||
"via_sharing_links_legacy": "",
|
||||
"view": "",
|
||||
"view_all": "",
|
||||
"view_all_plans": "",
|
||||
@@ -2315,6 +2328,8 @@
|
||||
"x_changes_in": "",
|
||||
"x_changes_in_plural": "",
|
||||
"x_libraries_accessed_in_this_project": "",
|
||||
"x_people_invited": "",
|
||||
"x_people_invited_plural": "",
|
||||
"x_price_for_first_month": "",
|
||||
"x_price_for_first_year": "",
|
||||
"x_price_for_y_months": "",
|
||||
|
||||
BIN
Binary file not shown.
@@ -15,6 +15,7 @@ export default /** @type {const} */ ([
|
||||
'cached',
|
||||
'call_split',
|
||||
'check',
|
||||
'check_circle',
|
||||
'chevron_left',
|
||||
'chevron_right',
|
||||
'close',
|
||||
@@ -24,6 +25,8 @@ export default /** @type {const} */ ([
|
||||
'delete_forever',
|
||||
'delete',
|
||||
'description',
|
||||
'domain',
|
||||
'edit',
|
||||
'edit_square',
|
||||
'error',
|
||||
'experiment',
|
||||
@@ -31,6 +34,8 @@ export default /** @type {const} */ ([
|
||||
'format_strikethrough',
|
||||
'forum',
|
||||
'functions',
|
||||
'globe',
|
||||
'group',
|
||||
'help',
|
||||
'image',
|
||||
'info',
|
||||
@@ -40,6 +45,7 @@ export default /** @type {const} */ ([
|
||||
'lightbulb_2',
|
||||
'lock',
|
||||
'lock_open',
|
||||
'mode_comment',
|
||||
'more_vert',
|
||||
'neurology',
|
||||
'note_add',
|
||||
@@ -67,6 +73,7 @@ export default /** @type {const} */ ([
|
||||
'turn_left',
|
||||
'upload_file',
|
||||
'upload',
|
||||
'visibility',
|
||||
'visibility_off',
|
||||
'web_asset',
|
||||
'whatshot',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ShareProjectContextValue } from '@/features/share-project-modal/components/share-project-modal'
|
||||
|
||||
function ErrorMessage({ error }: Pick<ShareProjectContextValue, 'error'>) {
|
||||
const { t } = useTranslation()
|
||||
switch (error) {
|
||||
case 'cannot_invite_non_user':
|
||||
return <>{t('cannot_invite_non_user')}</>
|
||||
|
||||
case 'cannot_verify_user_not_robot':
|
||||
return <>{t('cannot_verify_user_not_robot')}</>
|
||||
|
||||
case 'cannot_invite_self':
|
||||
return <>{t('cannot_invite_self')}</>
|
||||
|
||||
case 'invalid_email':
|
||||
return <>{t('invalid_email')}</>
|
||||
|
||||
case 'too_many_requests':
|
||||
return <>{t('too_many_requests')}</>
|
||||
|
||||
case 'invite_expired':
|
||||
return <>{t('invite_expired')}</>
|
||||
|
||||
case 'invite_resend_limit_hit':
|
||||
return <>{t('invite_resend_limit_hit')}</>
|
||||
|
||||
default:
|
||||
return <>{t('generic_something_went_wrong')}</>
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorMessage
|
||||
@@ -0,0 +1,7 @@
|
||||
type InvitedPeopleProps = object
|
||||
|
||||
function InvitedPeople(_props: InvitedPeopleProps) {
|
||||
return null
|
||||
}
|
||||
|
||||
export default InvitedPeople
|
||||
@@ -16,6 +16,7 @@ import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
type Tokens = {
|
||||
readAndWrite: string
|
||||
@@ -28,12 +29,13 @@ type Tokens = {
|
||||
type AccessLevel = 'private' | 'tokenBased' | 'readAndWrite' | 'readOnly'
|
||||
|
||||
export default function LinkSharing() {
|
||||
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const [showLinks, setShowLinks] = useState(true)
|
||||
const linkSharingEnabled =
|
||||
getMeta('ol-capabilities')?.includes('link-sharing')
|
||||
|
||||
const { monitorRequest } = useShareProjectContext()
|
||||
const { monitorRequest, projectAccess } = useShareProjectContext()
|
||||
|
||||
const { projectId, project } = useProjectContext()
|
||||
const { publicAccessLevel } = project || {}
|
||||
@@ -64,6 +66,13 @@ export default function LinkSharing() {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isSharingUpdatesEnabled) {
|
||||
if (projectAccess === 'linkSharing') {
|
||||
return <ReadAndWriteTokenLinks />
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
switch (publicAccessLevel) {
|
||||
// Private (with token-access available)
|
||||
case 'private':
|
||||
@@ -140,6 +149,20 @@ function PrivateSharing({
|
||||
)
|
||||
}
|
||||
|
||||
function useProjectTokens() {
|
||||
const { projectId } = useProjectContext()
|
||||
const [tokens, setTokens] = useState<Tokens | null>(null)
|
||||
const { signal } = useAbortController()
|
||||
|
||||
useEffect(() => {
|
||||
getJSON(`/project/${projectId}/tokens`, { signal })
|
||||
.then(data => setTokens(data))
|
||||
.catch(debugConsole.error)
|
||||
}, [projectId, signal])
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
function TokenBasedSharing({
|
||||
setAccessLevel,
|
||||
inflight,
|
||||
@@ -152,17 +175,7 @@ function TokenBasedSharing({
|
||||
showLinks: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [tokens, setTokens] = useState<Tokens | null>(null)
|
||||
|
||||
const { signal } = useAbortController()
|
||||
|
||||
useEffect(() => {
|
||||
getJSON(`/project/${projectId}/tokens`, { signal })
|
||||
.then(data => setTokens(data))
|
||||
.catch(debugConsole.error)
|
||||
}, [projectId, signal])
|
||||
const tokens = useProjectTokens()
|
||||
|
||||
return (
|
||||
<OLRow className="public-access-level">
|
||||
@@ -191,24 +204,8 @@ function TokenBasedSharing({
|
||||
</OLCol>
|
||||
{showLinks && (
|
||||
<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>
|
||||
<AccessTokenEditDisplayArea tokens={tokens} />
|
||||
<AccessTokenViewDisplayArea tokens={tokens} />
|
||||
</OLCol>
|
||||
)}
|
||||
</OLRow>
|
||||
@@ -249,32 +246,26 @@ function LegacySharing({
|
||||
)
|
||||
}
|
||||
|
||||
export function ReadOnlyTokenLink() {
|
||||
const { t } = useTranslation()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [tokens, setTokens] = useState<Tokens | null>(null)
|
||||
|
||||
const { signal } = useAbortController()
|
||||
|
||||
useEffect(() => {
|
||||
getJSON(`/project/${projectId}/tokens`, { signal })
|
||||
.then(data => setTokens(data))
|
||||
.catch(debugConsole.error)
|
||||
}, [projectId, signal])
|
||||
export function ReadAndWriteTokenLinks() {
|
||||
const tokens = useProjectTokens()
|
||||
|
||||
return (
|
||||
<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
|
||||
token={tokens?.readOnly}
|
||||
tokenHashPrefix={tokens?.readOnlyHashPrefix}
|
||||
path="/read/"
|
||||
tooltipId="tooltip-copy-link-ro"
|
||||
/>
|
||||
</div>
|
||||
<AccessTokenEditDisplayArea tokens={tokens} />
|
||||
<AccessTokenViewDisplayArea tokens={tokens} />
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)
|
||||
}
|
||||
|
||||
export function ReadOnlyTokenLink() {
|
||||
const tokens = useProjectTokens()
|
||||
|
||||
return (
|
||||
<OLRow className="public-access-level">
|
||||
<OLCol className="access-token-display-area">
|
||||
<AccessTokenViewDisplayArea tokens={tokens} />
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)
|
||||
@@ -336,3 +327,39 @@ function LinkSharingInfo() {
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function AccessTokenEditDisplayArea({ tokens }: { tokens: Tokens | null }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="access-token-wrapper">
|
||||
<strong className="access-token-wrapper-title">
|
||||
{t('anyone_with_link_can_edit')}
|
||||
</strong>
|
||||
<AccessToken
|
||||
token={tokens?.readAndWrite}
|
||||
tokenHashPrefix={tokens?.readAndWriteHashPrefix}
|
||||
path="/"
|
||||
tooltipId="tooltip-copy-link-rw"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AccessTokenViewDisplayArea({ tokens }: { tokens: Tokens | null }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="access-token-wrapper">
|
||||
<strong className="access-token-wrapper-title">
|
||||
{t('anyone_with_link_can_view')}
|
||||
</strong>
|
||||
<AccessToken
|
||||
token={tokens?.readOnly}
|
||||
tokenHashPrefix={tokens?.readOnlyHashPrefix}
|
||||
path="/read/"
|
||||
tooltipId="tooltip-copy-link-ro"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownDivider,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
|
||||
import LinkSharing from '@/features/share-project-modal/components/link-sharing'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||
import MemberPrivileges from '@/features/share-project-modal/components/member-privileges'
|
||||
import RemoveSharingLinksModal from '@/features/share-project-modal/components/remove-sharing-links-modal'
|
||||
import {
|
||||
ProjectAccessType,
|
||||
useShareProjectContext,
|
||||
} from '@/features/share-project-modal/components/share-project-modal'
|
||||
|
||||
type ProjectAccessProps = {
|
||||
setIsInvitedPeopleScreen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
invitedPeopleCount: number
|
||||
}
|
||||
|
||||
export type PendingAccessType = Exclude<ProjectAccessType, 'linkSharing'>
|
||||
|
||||
function ProjectAccess({
|
||||
setIsInvitedPeopleScreen,
|
||||
invitedPeopleCount,
|
||||
}: ProjectAccessProps) {
|
||||
const { t } = useTranslation()
|
||||
const [pendingAccess, setPendingAccess] = useState<PendingAccessType | null>(
|
||||
null
|
||||
)
|
||||
// TODO set initial state
|
||||
const [privileges, setPrivileges] =
|
||||
useState<Exclude<PermissionsLevel, 'owner'>>('readOnly')
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
// TODO set company name
|
||||
const companyName = 'XYZ'
|
||||
|
||||
const {
|
||||
monitorRequest,
|
||||
setSuccessActionMessage,
|
||||
projectAccess,
|
||||
setProjectAccess,
|
||||
} = useShareProjectContext()
|
||||
|
||||
const handleAccessChange = (newAccess: PendingAccessType) => {
|
||||
setPendingAccess(null)
|
||||
|
||||
monitorRequest(
|
||||
() =>
|
||||
// TODO: replace with real API call
|
||||
new Promise(resolve => setTimeout(resolve, 1000))
|
||||
).then(() => {
|
||||
setProjectAccess(newAccess)
|
||||
setSuccessActionMessage(t('access_updated'))
|
||||
})
|
||||
}
|
||||
|
||||
const onAccessSelect = (eventKey: ProjectAccessType) => {
|
||||
if (projectAccess === 'linkSharing' && eventKey !== 'linkSharing') {
|
||||
// Legacy link sharing: show confirmation first
|
||||
setPendingAccess(eventKey as PendingAccessType)
|
||||
} else {
|
||||
// Non-legacy: fire request directly
|
||||
handleAccessChange(eventKey as PendingAccessType)
|
||||
}
|
||||
}
|
||||
|
||||
const onPrivilegesChange = (eventKey: Exclude<PermissionsLevel, 'owner'>) => {
|
||||
monitorRequest(
|
||||
() =>
|
||||
// TODO: replace with real API call
|
||||
new Promise(resolve => setTimeout(resolve, 1000))
|
||||
).then(() => {
|
||||
setPrivileges(eventKey)
|
||||
setSuccessActionMessage(t('access_updated'))
|
||||
})
|
||||
}
|
||||
|
||||
const getProjectAccessDropdownToggleText = () => {
|
||||
switch (projectAccess) {
|
||||
case 'linkSharing':
|
||||
return t('via_sharing_links_legacy')
|
||||
case 'onlyInvitedPeople':
|
||||
return t('only_invited_people')
|
||||
case 'anyoneInXyzWithTheLink':
|
||||
return t('anyone_in_x_with_the_link', { companyName })
|
||||
case 'anyoneWithTheLink':
|
||||
return t('anyone_with_the_link')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="h4 fw-normal mt-3 mb-2 pt-1">{t('project_access')}</h3>
|
||||
<ShareProjectModalRow>
|
||||
<div className="d-inline-flex align-items-center h5 m-0 gap-2">
|
||||
<MaterialIcon type="group" unfilled />
|
||||
<div className="px-2 fw-normal">
|
||||
{t('x_people_invited', { count: invitedPeopleCount })}
|
||||
</div>
|
||||
</div>
|
||||
<OLButton
|
||||
variant="ghost"
|
||||
trailingIcon="chevron_right"
|
||||
onClick={() => setIsInvitedPeopleScreen(true)}
|
||||
>
|
||||
{t('manage_access')}
|
||||
</OLButton>
|
||||
</ShareProjectModalRow>
|
||||
{projectAccess && (
|
||||
<ShareProjectModalRow>
|
||||
<div className="d-inline-flex align-items-center h5 m-0">
|
||||
{projectAccess === 'linkSharing' && <MaterialIcon type="link" />}
|
||||
{projectAccess === 'onlyInvitedPeople' && (
|
||||
<MaterialIcon type="lock" unfilled />
|
||||
)}
|
||||
{projectAccess === 'anyoneInXyzWithTheLink' && (
|
||||
<MaterialIcon type="domain" unfilled />
|
||||
)}
|
||||
{projectAccess === 'anyoneWithTheLink' && (
|
||||
<MaterialIcon type="globe" unfilled />
|
||||
)}
|
||||
<Dropdown onSelect={onAccessSelect}>
|
||||
<DropdownToggle
|
||||
variant="ghost"
|
||||
className="d-flex align-items-center gap-2 no-default-caret"
|
||||
>
|
||||
{getProjectAccessDropdownToggleText()}
|
||||
<MaterialIcon type="keyboard_arrow_down" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{projectAccess === 'linkSharing' && (
|
||||
<>
|
||||
<DropdownListItem className="d-flex align-items-center">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
eventKey="linkSharing"
|
||||
leadingIcon={<MaterialIcon type="link" />}
|
||||
trailingIcon={
|
||||
projectAccess === 'linkSharing' ? 'check' : undefined
|
||||
}
|
||||
active={projectAccess === 'linkSharing'}
|
||||
>
|
||||
{t('via_sharing_links_legacy')}
|
||||
</DropdownItem>
|
||||
</DropdownListItem>
|
||||
<DropdownDivider />
|
||||
</>
|
||||
)}
|
||||
<DropdownListItem className="d-flex align-items-center">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
eventKey="onlyInvitedPeople"
|
||||
leadingIcon={<MaterialIcon type="lock" unfilled />}
|
||||
trailingIcon={
|
||||
projectAccess === 'onlyInvitedPeople'
|
||||
? 'check'
|
||||
: undefined
|
||||
}
|
||||
active={projectAccess === 'onlyInvitedPeople'}
|
||||
>
|
||||
{t('only_invited_people')}
|
||||
</DropdownItem>
|
||||
</DropdownListItem>
|
||||
<DropdownListItem className="d-flex align-items-center">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
eventKey="anyoneInXyzWithTheLink"
|
||||
leadingIcon={<MaterialIcon type="domain" unfilled />}
|
||||
trailingIcon={
|
||||
projectAccess === 'anyoneInXyzWithTheLink'
|
||||
? 'check'
|
||||
: undefined
|
||||
}
|
||||
active={projectAccess === 'anyoneInXyzWithTheLink'}
|
||||
>
|
||||
{t('anyone_in_x_with_the_link', { companyName })}
|
||||
</DropdownItem>
|
||||
</DropdownListItem>
|
||||
<DropdownListItem className="d-flex align-items-center gap-2">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
eventKey="anyoneWithTheLink"
|
||||
leadingIcon={<MaterialIcon type="globe" unfilled />}
|
||||
trailingIcon={
|
||||
projectAccess === 'anyoneWithTheLink'
|
||||
? 'check'
|
||||
: undefined
|
||||
}
|
||||
active={projectAccess === 'anyoneWithTheLink'}
|
||||
>
|
||||
{t('anyone_with_the_link')}
|
||||
</DropdownItem>
|
||||
<OLTooltip
|
||||
id="tooltip-anyone-with-link"
|
||||
description={t('not_permitted_by_your_organization')}
|
||||
overlayProps={{ placement: 'left' }}
|
||||
>
|
||||
<span style={{ cursor: 'default' }}>
|
||||
<MaterialIcon
|
||||
type="info"
|
||||
unfilled
|
||||
className="align-middle px-2"
|
||||
/>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
</DropdownListItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{pendingAccess && (
|
||||
<RemoveSharingLinksModal
|
||||
pendingAccess={pendingAccess}
|
||||
onCancel={() => setPendingAccess(null)}
|
||||
onConfirm={() => handleAccessChange(pendingAccess)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{projectAccess !== 'linkSharing' && (
|
||||
<Dropdown align="end" onSelect={onPrivilegesChange}>
|
||||
<DropdownToggle
|
||||
variant="ghost"
|
||||
className="d-flex align-items-center gap-2 no-default-caret"
|
||||
>
|
||||
<MemberPrivileges privileges={privileges} />
|
||||
<MaterialIcon type="keyboard_arrow_down" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<OLDropdownMenuItem
|
||||
as="button"
|
||||
eventKey="readAndWrite"
|
||||
leadingIcon={<MaterialIcon type="edit" unfilled />}
|
||||
active={privileges === 'readAndWrite'}
|
||||
trailingIcon={
|
||||
privileges === 'readAndWrite' ? 'check' : undefined
|
||||
}
|
||||
>
|
||||
{t('editor')}
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
as="button"
|
||||
eventKey="review"
|
||||
leadingIcon={<MaterialIcon type="mode_comment" unfilled />}
|
||||
active={privileges === 'review'}
|
||||
trailingIcon={privileges === 'review' ? 'check' : undefined}
|
||||
>
|
||||
{t('reviewer')}
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
as="button"
|
||||
eventKey="readOnly"
|
||||
leadingIcon={<MaterialIcon type="visibility" unfilled />}
|
||||
active={privileges === 'readOnly'}
|
||||
trailingIcon={privileges === 'readOnly' ? 'check' : undefined}
|
||||
>
|
||||
{t('viewer')}
|
||||
</OLDropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</ShareProjectModalRow>
|
||||
)}
|
||||
|
||||
{isProjectOwner && <LinkSharing />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectAccess
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { PendingAccessType } from '@/features/share-project-modal/components/project-access'
|
||||
|
||||
type RemoveSharingLinksModalProps = {
|
||||
pendingAccess: PendingAccessType
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
function RemoveSharingLinksModal({
|
||||
pendingAccess,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: RemoveSharingLinksModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let confirmationModalBodyText = null
|
||||
if (pendingAccess === 'onlyInvitedPeople') {
|
||||
confirmationModalBodyText = t(
|
||||
'this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link'
|
||||
)
|
||||
} else if (
|
||||
pendingAccess === 'anyoneInXyzWithTheLink' ||
|
||||
pendingAccess === 'anyoneWithTheLink'
|
||||
) {
|
||||
confirmationModalBodyText = t(
|
||||
'this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<OLModal show onHide={onCancel} size="sm" backdrop={false}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('remove_original_sharing_links')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>{confirmationModalBodyText}</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={onCancel}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton variant="danger" onClick={onConfirm}>
|
||||
{t('remove_original_links')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveSharingLinksModal
|
||||
@@ -3,6 +3,8 @@ import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
|
||||
import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade'
|
||||
import AccessLevelsChanged from './access-levels-changed'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import classnames from 'classnames'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export default function SendInvites({
|
||||
canAddCollaborators,
|
||||
@@ -15,8 +17,18 @@ export default function SendInvites({
|
||||
haveAnyEditorsBeenDowngraded: boolean
|
||||
somePendingEditorsResolved: boolean
|
||||
}) {
|
||||
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
|
||||
|
||||
if (isSharingUpdatesEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<OLRow className="invite-controls">
|
||||
<OLRow
|
||||
className={classnames('invite-controls', {
|
||||
'pb-3': isSharingUpdatesEnabled,
|
||||
})}
|
||||
>
|
||||
{hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && (
|
||||
<AddCollaboratorsUpgrade />
|
||||
)}
|
||||
|
||||
+69
-28
@@ -10,11 +10,27 @@ import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useMemo } from 'react'
|
||||
import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import ErrorMessage from '@/features/share-project-modal/components/error-message'
|
||||
import ProjectAccess from '@/features/share-project-modal/components/project-access'
|
||||
import InvitedPeople from '@/features/share-project-modal/components/invited-people'
|
||||
|
||||
export default function ShareModalBody() {
|
||||
type ShareModalBodyProps = {
|
||||
isInvitedPeopleScreen: boolean
|
||||
setIsInvitedPeopleScreen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function ShareModalBody({
|
||||
isInvitedPeopleScreen,
|
||||
setIsInvitedPeopleScreen,
|
||||
error,
|
||||
}: ShareModalBodyProps) {
|
||||
const { project, features } = useProjectContext()
|
||||
const { members, invites } = project || {}
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
|
||||
|
||||
// whether the project has not reached the collaborator limit
|
||||
const canAddCollaborators = useMemo(() => {
|
||||
@@ -108,37 +124,62 @@ export default function ShareModalBody() {
|
||||
) : (
|
||||
<SendInvitesNotice />
|
||||
)}
|
||||
{isProjectOwner && <LinkSharing />}
|
||||
|
||||
<OwnerInfo />
|
||||
{isSharingUpdatesEnabled ? (
|
||||
<>
|
||||
{error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={<ErrorMessage error={error} />}
|
||||
/>
|
||||
)}
|
||||
{isInvitedPeopleScreen || !isProjectOwner ? (
|
||||
<InvitedPeople />
|
||||
) : (
|
||||
<ProjectAccess
|
||||
setIsInvitedPeopleScreen={setIsInvitedPeopleScreen}
|
||||
// adding +1 for the project owner
|
||||
invitedPeopleCount={
|
||||
sortedMembers.length + (invites || []).length + 1
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isProjectOwner && <LinkSharing />}
|
||||
|
||||
{sortedMembers.map(member =>
|
||||
isProjectOwner ? (
|
||||
<EditMember
|
||||
key={member._id}
|
||||
member={member}
|
||||
hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
|
||||
hasBeenDowngraded={Boolean(
|
||||
member.pendingEditor || member.pendingReviewer
|
||||
)}
|
||||
canAddCollaborators={canAddCollaborators}
|
||||
isReviewerOnFreeProject={
|
||||
member.privileges === 'review' && !features.trackChanges
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ViewMember key={member._id} member={member} />
|
||||
)
|
||||
<OwnerInfo />
|
||||
|
||||
{sortedMembers.map(member =>
|
||||
isProjectOwner ? (
|
||||
<EditMember
|
||||
key={member._id}
|
||||
member={member}
|
||||
hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
|
||||
hasBeenDowngraded={Boolean(
|
||||
member.pendingEditor || member.pendingReviewer
|
||||
)}
|
||||
canAddCollaborators={canAddCollaborators}
|
||||
isReviewerOnFreeProject={
|
||||
member.privileges === 'review' && !features.trackChanges
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ViewMember key={member._id} member={member} />
|
||||
)
|
||||
)}
|
||||
|
||||
{(invites || []).map(invite => (
|
||||
<Invite
|
||||
key={invite._id}
|
||||
invite={invite}
|
||||
isProjectOwner={isProjectOwner}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(invites || []).map(invite => (
|
||||
<Invite
|
||||
key={invite._id}
|
||||
invite={invite}
|
||||
isProjectOwner={isProjectOwner}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!getMeta('ol-ExposedSettings').recaptchaDisabled?.invite && (
|
||||
<RecaptchaConditions />
|
||||
)}
|
||||
|
||||
+70
-37
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { lazy, Suspense, useState } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
|
||||
import {
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import ErrorMessage from '@/features/share-project-modal/components/error-message'
|
||||
import classNames from 'classnames'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { useShareProjectContext } from '@/features/share-project-modal/components/share-project-modal'
|
||||
|
||||
const ReadOnlyTokenLink = lazy(() =>
|
||||
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
|
||||
@@ -29,6 +34,7 @@ type ShareProjectModalContentProps = {
|
||||
animation: boolean
|
||||
inFlight: boolean
|
||||
error: string | undefined
|
||||
projectName?: string
|
||||
}
|
||||
|
||||
export default function ShareProjectModalContent({
|
||||
@@ -37,27 +43,59 @@ export default function ShareProjectModalContent({
|
||||
animation,
|
||||
inFlight,
|
||||
error,
|
||||
projectName,
|
||||
}: ShareProjectModalContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
|
||||
const [isInvitedPeopleScreen, setIsInvitedPeopleScreen] = useState(false)
|
||||
const { successActionMessage } = useShareProjectContext()
|
||||
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={cancel} animation={animation}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('share_project')}</OLModalTitle>
|
||||
<div className="d-flex flex-grow-1 justify-content-between">
|
||||
{isSharingUpdatesEnabled && isInvitedPeopleScreen ? (
|
||||
<OLButton
|
||||
variant="ghost"
|
||||
onClick={() => setIsInvitedPeopleScreen(false)}
|
||||
leadingIcon="arrow_back_ios_new"
|
||||
>
|
||||
{t('back')}
|
||||
</OLButton>
|
||||
) : (
|
||||
<OLModalTitle>
|
||||
{isSharingUpdatesEnabled && projectName
|
||||
? t('share_project_name', { projectName })
|
||||
: t('share_project')}
|
||||
</OLModalTitle>
|
||||
)}
|
||||
</div>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody className="modal-body-share modal-link-share">
|
||||
<div className="container-fluid">
|
||||
<OLModalBody
|
||||
className={classNames('modal-body-share modal-link-share', {
|
||||
'modal-redesign': isSharingUpdatesEnabled,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'container-fluid': !isSharingUpdatesEnabled,
|
||||
})}
|
||||
>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
|
||||
{isRestrictedTokenMember ? (
|
||||
<ReadOnlyTokenLink />
|
||||
) : (
|
||||
<ShareModalBody />
|
||||
<ShareModalBody
|
||||
isInvitedPeopleScreen={isInvitedPeopleScreen}
|
||||
setIsInvitedPeopleScreen={setIsInvitedPeopleScreen}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
{error && (
|
||||
{!isSharingUpdatesEnabled && error && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={<ErrorMessage error={error} />}
|
||||
@@ -68,12 +106,36 @@ export default function ShareProjectModalContent({
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<div className="me-auto">{inFlight && <OLSpinner size="sm" />}</div>
|
||||
<div className="d-flex flex-grow-1 flex-wrap gap-2">
|
||||
{isSharingUpdatesEnabled ? (
|
||||
<>
|
||||
{successActionMessage && (
|
||||
<div className="ms-auto px-3 align-self-center">
|
||||
<div
|
||||
className="d-flex gap-3 align-items-center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<MaterialIcon
|
||||
unfilled
|
||||
type="check_circle"
|
||||
className="text-success"
|
||||
/>
|
||||
<span>{successActionMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
inFlight && <OLSpinner size="sm" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ClickableElementEnhancer
|
||||
onClick={cancel}
|
||||
as={OLButton}
|
||||
variant="secondary"
|
||||
variant={isSharingUpdatesEnabled ? 'ghost' : 'secondary'}
|
||||
isLoading={isSharingUpdatesEnabled && inFlight}
|
||||
disabled={inFlight}
|
||||
>
|
||||
{t('close')}
|
||||
@@ -82,32 +144,3 @@ export default function ShareProjectModalContent({
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorMessage({ error }: Pick<ShareProjectModalContentProps, 'error'>) {
|
||||
const { t } = useTranslation()
|
||||
switch (error) {
|
||||
case 'cannot_invite_non_user':
|
||||
return <>{t('cannot_invite_non_user')}</>
|
||||
|
||||
case 'cannot_verify_user_not_robot':
|
||||
return <>{t('cannot_verify_user_not_robot')}</>
|
||||
|
||||
case 'cannot_invite_self':
|
||||
return <>{t('cannot_invite_self')}</>
|
||||
|
||||
case 'invalid_email':
|
||||
return <>{t('invalid_email')}</>
|
||||
|
||||
case 'too_many_requests':
|
||||
return <>{t('too_many_requests')}</>
|
||||
|
||||
case 'invite_expired':
|
||||
return <>{t('invite_expired')}</>
|
||||
|
||||
case 'invite_resend_limit_hit':
|
||||
return <>{t('invite_resend_limit_hit')}</>
|
||||
|
||||
default:
|
||||
return <>{t('generic_something_went_wrong')}</>
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
const ShareProjectModalRow: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="d-flex justify-content-between flex-wrap py-2">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareProjectModalRow
|
||||
+40
-1
@@ -12,7 +12,13 @@ import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
|
||||
type ShareProjectContextValue = {
|
||||
export type ProjectAccessType =
|
||||
| 'linkSharing'
|
||||
| 'onlyInvitedPeople'
|
||||
| 'anyoneInXyzWithTheLink'
|
||||
| 'anyoneWithTheLink'
|
||||
|
||||
export type ShareProjectContextValue = {
|
||||
monitorRequest: <T extends Promise<unknown>>(request: () => T) => T
|
||||
inFlight: boolean
|
||||
setInFlight: React.Dispatch<
|
||||
@@ -22,6 +28,14 @@ type ShareProjectContextValue = {
|
||||
setError: React.Dispatch<
|
||||
React.SetStateAction<ShareProjectContextValue['error']>
|
||||
>
|
||||
successActionMessage: string | undefined
|
||||
setSuccessActionMessage: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>
|
||||
projectAccess: ProjectAccessType | undefined
|
||||
setProjectAccess: React.Dispatch<
|
||||
React.SetStateAction<ProjectAccessType | undefined>
|
||||
>
|
||||
}
|
||||
|
||||
const SHOW_MODAL_COOLDOWN_PERIOD = 24 * 60 * 60 * 1000 // 24 hours
|
||||
@@ -58,9 +72,28 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
const [inFlight, setInFlight] =
|
||||
useState<ShareProjectContextValue['inFlight']>(false)
|
||||
const [error, setError] = useState<ShareProjectContextValue['error']>()
|
||||
const [projectAccess, setProjectAccess] = useState<
|
||||
ProjectAccessType | undefined
|
||||
>()
|
||||
const [successActionMessage, setSuccessActionMessage] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
|
||||
const { project, projectId } = useProjectContext()
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
const { publicAccessLevel } = project || {}
|
||||
|
||||
// TODO: handle initial state for projectAccess
|
||||
useEffect(() => {
|
||||
if (!projectAccess) {
|
||||
if (publicAccessLevel === 'tokenBased') {
|
||||
// consider a legacy link sharing is enabled
|
||||
setProjectAccess('linkSharing')
|
||||
} else {
|
||||
setProjectAccess('onlyInvitedPeople')
|
||||
}
|
||||
}
|
||||
}, [projectAccess, publicAccessLevel])
|
||||
|
||||
const { splitTestVariants } = useSplitTestContext()
|
||||
|
||||
@@ -120,6 +153,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
const cancel = useCallback(() => {
|
||||
if (!inFlight) {
|
||||
handleHide()
|
||||
setSuccessActionMessage(undefined)
|
||||
}
|
||||
}, [handleHide, inFlight])
|
||||
|
||||
@@ -157,6 +191,10 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
setInFlight,
|
||||
error,
|
||||
setError,
|
||||
successActionMessage,
|
||||
setSuccessActionMessage,
|
||||
projectAccess,
|
||||
setProjectAccess,
|
||||
}}
|
||||
>
|
||||
<ShareProjectModalContent
|
||||
@@ -165,6 +203,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
error={error}
|
||||
inFlight={inFlight}
|
||||
show={show}
|
||||
projectName={project.name}
|
||||
/>
|
||||
</ShareProjectContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export default function DropdownListItem({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return <li role="none">{children}</li>
|
||||
return (
|
||||
<li role="none" className={className}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -294,3 +294,11 @@ $dropdown-item-min-height: 36px;
|
||||
.dropdown-item-wide {
|
||||
min-width: 264px;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
&.no-default-caret {
|
||||
&::after {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.modal-body-share {
|
||||
h3 {
|
||||
&:not(.modal-redesign) h3 {
|
||||
border-bottom: 1px solid var(--neutral-30);
|
||||
padding-bottom: calc(var(--line-height-03) / 4);
|
||||
margin: 0;
|
||||
@@ -159,6 +159,29 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-redesign {
|
||||
.public-access-level {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
.access-token-display-area {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.access-token-wrapper-title {
|
||||
color: var(--content-primary) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.access-token {
|
||||
code {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button:focus-within {
|
||||
@@ -199,12 +222,22 @@
|
||||
|
||||
.add-collaborator-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-03);
|
||||
|
||||
.select-wrapper {
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
&:not(.add-collaborator-controls-legacy) .dropdown-menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control-lg {
|
||||
min-height: initial;
|
||||
padding-top: calc(#{$input-padding-y-lg} - #{$input-border-width});
|
||||
padding-bottom: calc(#{$input-padding-y-lg} - #{$input-border-width});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"access_denied": "Access Denied",
|
||||
"access_edit_your_projects": "Access and edit your projects",
|
||||
"access_levels_changed": "Access levels changed",
|
||||
"access_updated": "Access updated.",
|
||||
"account": "Account",
|
||||
"account_already_managed": "Your account is already managed.",
|
||||
"account_associated_with_x": "Your account is associated with __companyName__.",
|
||||
@@ -197,8 +198,10 @@
|
||||
"anonymous": "Anonymous",
|
||||
"anyone": "Anyone",
|
||||
"anyone_in_x": "Anyone in __groupName__",
|
||||
"anyone_in_x_with_the_link": "Anyone in __companyName__ with the link",
|
||||
"anyone_with_link_can_edit": "Anyone with this link can edit this project",
|
||||
"anyone_with_link_can_view": "Anyone with this link can view this project",
|
||||
"anyone_with_the_link": "Anyone with the link",
|
||||
"app_on_x": "__appName__ on __social__",
|
||||
"appearance": "Appearance",
|
||||
"apply": "Apply ",
|
||||
@@ -1461,6 +1464,7 @@
|
||||
"make_owner": "Make owner",
|
||||
"make_primary": "Make primary",
|
||||
"make_private": "Make private",
|
||||
"manage_access": "Manage access",
|
||||
"manage_beta_program_membership": "Manage beta program membership",
|
||||
"manage_email_preferences": "Manage email preferences",
|
||||
"manage_files_from_your_dropbox_folder": "Manage files from your Dropbox folder",
|
||||
@@ -1646,6 +1650,7 @@
|
||||
"not_found_error_from_the_supplied_url": "The link to open this content on Overleaf pointed to a file that could not be found. If this keeps happening for links on a particular site, please report this to them.",
|
||||
"not_managed": "Not managed",
|
||||
"not_now": "Not now",
|
||||
"not_permitted_by_your_organization": "Not permitted by your organization",
|
||||
"not_registered": "Not registered",
|
||||
"note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
|
||||
"notification": "Notification",
|
||||
@@ -1687,6 +1692,7 @@
|
||||
"only_group_admin_or_managers_can_delete_your_account_7": "Only your group admin or group managers will be able to delete your account or change your account into an unmanaged account.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_9": "Once you have become a managed user, <0>you yourself cannot change it back to an unmanaged account</0>. <1>Learn more about managed Overleaf accounts.</1>",
|
||||
"only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.",
|
||||
"only_invited_people": "Only invited people",
|
||||
"only_project_owner_can_link_github": "Only the project owner can link this project to a GitHub repository. Please contact the project owner if you would like to set this up.",
|
||||
"open_action_menu": "Open __name__ action menu",
|
||||
"open_advanced_reference_search": "Open advanced reference search",
|
||||
@@ -1914,6 +1920,7 @@
|
||||
"professional": "Professional",
|
||||
"progress_bar_percentage": "Progress bar from 0 to 100%",
|
||||
"project": "project",
|
||||
"project_access": "Project access",
|
||||
"project_actions": "Project actions",
|
||||
"project_approaching_file_limit": "This project is approaching the file limit",
|
||||
"project_categories_tags": "Project categories and tags",
|
||||
@@ -2048,6 +2055,8 @@
|
||||
"remove_link": "Remove link",
|
||||
"remove_manager": "Remove manager",
|
||||
"remove_or_replace_figure": "Remove or replace figure",
|
||||
"remove_original_links": "Remove original links",
|
||||
"remove_original_sharing_links": "Remove original sharing links?",
|
||||
"remove_secondary_email_addresses": "Remove any secondary email addresses associated with your account. <0>Remove them in account settings.</0>",
|
||||
"remove_secondary_email_addresses_except_saml_email": "Remove all email addresses except <0>__institutionEmail__</0> from your Overleaf account. <1>Remove them in account settings.</1>",
|
||||
"remove_sso_login_option": "Remove the SSO login option for your users.",
|
||||
@@ -2272,6 +2281,7 @@
|
||||
"share": "Share",
|
||||
"share_feedback": "Share feedback",
|
||||
"share_project": "Share Project",
|
||||
"share_project_name": "Share __projectName__",
|
||||
"shared_with_you": "Shared with you",
|
||||
"sharelatex_beta_program": "__appName__ beta program",
|
||||
"sharing_permissions": "Sharing permissions",
|
||||
@@ -2587,6 +2597,8 @@
|
||||
"this_action_cannot_be_undone": "This action cannot be undone.",
|
||||
"this_action_will_also_disable_domain_capture": "This action will also disable domain capture.",
|
||||
"this_address_will_be_shown_on_the_invoice": "This address will be shown on the invoice",
|
||||
"this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link": "This change will permanently remove your original sharing links. You can still share a new link to the project when you choose a relevant sharing option.",
|
||||
"this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link": "This change will permanently remove your original sharing links. You’ll need to share the new link instead.",
|
||||
"this_could_be_because_we_cant_support_some_elements_of_the_table": "This could be because we can’t yet support some elements of the table in the table preview. Or there may be an error in the table’s LaTeX code.",
|
||||
"this_field_is_required": "This field is required",
|
||||
"this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
|
||||
@@ -2881,6 +2893,7 @@
|
||||
"verify_email_address_before_enabling_managed_users": "You need to verify your email address before enabling managed users.",
|
||||
"verify_your_email_address": "Verify your email address",
|
||||
"verifying_domain": "Verifying domain",
|
||||
"via_sharing_links_legacy": "Via sharing links (legacy)",
|
||||
"via_support": "via support",
|
||||
"view": "View",
|
||||
"view_all": "View all",
|
||||
@@ -2975,6 +2988,8 @@
|
||||
"x_changes_in": "__count__ change in",
|
||||
"x_changes_in_plural": "__count__ changes in",
|
||||
"x_libraries_accessed_in_this_project": "__provider__ libraries accessed in this project",
|
||||
"x_people_invited": "__count__ person invited",
|
||||
"x_people_invited_plural": "__count__ people invited",
|
||||
"x_price_for_first_month": "<0>__price__</0> for your first month",
|
||||
"x_price_for_first_year": "<0>__price__</0> for your first year",
|
||||
"x_price_for_y_months": "<0>__price__</0> for your first __discountMonths__ months",
|
||||
|
||||
Reference in New Issue
Block a user