mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-15 19:37:30 +00:00
Add optional personal access tokens for git bridge (#15209)
GitOrigin-RevId: 50d4c0e11728e014e81172c062a3b22fefa6286c
This commit is contained in:
parent
6408d150d5
commit
749aef1c6f
20 changed files with 198 additions and 73 deletions
|
@ -648,6 +648,21 @@ const ProjectController = {
|
|||
}
|
||||
)
|
||||
},
|
||||
personalAccessTokenAssignment(cb) {
|
||||
SplitTestHandler.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'personal-access-token',
|
||||
(error, assignment) => {
|
||||
// do not fail editor load if assignment fails
|
||||
if (error) {
|
||||
cb(null, { variant: 'default' })
|
||||
} else {
|
||||
cb(null, assignment)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
historyViewAssignment(cb) {
|
||||
SplitTestHandler.getAssignment(
|
||||
req,
|
||||
|
@ -716,6 +731,7 @@ const ProjectController = {
|
|||
historyViewAssignment,
|
||||
reviewPanelAssignment,
|
||||
idePageAssignment,
|
||||
personalAccessTokenAssignment,
|
||||
projectTags,
|
||||
}
|
||||
) => {
|
||||
|
@ -821,6 +837,10 @@ const ProjectController = {
|
|||
!Features.hasFeature('saas') ||
|
||||
req.query?.personal_access_token === 'true'
|
||||
|
||||
const optionalPersonalAccessToken =
|
||||
!showPersonalAccessToken &&
|
||||
personalAccessTokenAssignment.variant === 'enabled' // `?personal-access-token=enabled`
|
||||
|
||||
const idePageReact = idePageAssignment.variant === 'react'
|
||||
|
||||
const template =
|
||||
|
@ -903,6 +923,7 @@ const ProjectController = {
|
|||
isReviewPanelReact: reviewPanelAssignment.variant === 'react',
|
||||
idePageReact,
|
||||
showPersonalAccessToken,
|
||||
optionalPersonalAccessToken,
|
||||
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
||||
projectTags,
|
||||
})
|
||||
|
|
|
@ -10,6 +10,7 @@ const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
|
|||
const _ = require('lodash')
|
||||
const { expressify } = require('../../util/promises')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
|
||||
async function settingsPage(req, res) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
@ -69,8 +70,27 @@ async function settingsPage(req, res) {
|
|||
|
||||
const showPersonalAccessToken =
|
||||
!Features.hasFeature('saas') || req.query?.personal_access_token === 'true'
|
||||
|
||||
// if not already enabled, use a split test to determine whether to offer personal access tokens
|
||||
let optionalPersonalAccessToken = false
|
||||
if (!showPersonalAccessToken) {
|
||||
try {
|
||||
const { variant } = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'personal-access-token'
|
||||
)
|
||||
optionalPersonalAccessToken = variant === 'enabled' // `?personal-access-token=enabled`
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
'Failed to get personal-access-token split test assignment'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let personalAccessTokens
|
||||
if (showPersonalAccessToken) {
|
||||
if (showPersonalAccessToken || optionalPersonalAccessToken) {
|
||||
try {
|
||||
// require this here because module may not be included in some versions
|
||||
const PersonalAccessTokenManager = require('../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager')
|
||||
|
@ -133,6 +153,7 @@ async function settingsPage(req, res) {
|
|||
thirdPartyIds: UserPagesController._restructureThirdPartyIds(user),
|
||||
projectSyncSuccessMessage,
|
||||
showPersonalAccessToken,
|
||||
optionalPersonalAccessToken,
|
||||
personalAccessTokens,
|
||||
emailAddressLimit: Settings.emailAddressLimit,
|
||||
isManagedAccount: !!req.managedBy,
|
||||
|
|
|
@ -36,6 +36,7 @@ meta(name="ol-showSupport", data-type="boolean" content=showSupport)
|
|||
meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro)
|
||||
meta(name="ol-showCM6SwitchAwaySurvey", data-type="boolean" content=showCM6SwitchAwaySurvey)
|
||||
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
|
||||
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
|
||||
meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact)
|
||||
meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
|
||||
meta(name="ol-mathJax3Path" content=mathJax3Path)
|
||||
|
|
|
@ -23,6 +23,7 @@ block append meta
|
|||
meta(name="ol-github" data-type="json" content=github)
|
||||
meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
|
||||
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
|
||||
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
|
||||
meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
|
||||
meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
|
||||
meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
|
||||
|
|
|
@ -400,9 +400,11 @@
|
|||
"git_bridge_modal_click_generate": "",
|
||||
"git_bridge_modal_description": "",
|
||||
"git_bridge_modal_enter_authentication_token": "",
|
||||
"git_bridge_modal_git_authentication_tokens": "",
|
||||
"git_bridge_modal_see_once": "",
|
||||
"git_bridge_modal_tokens_description": "",
|
||||
"git_bridge_modal_use_previous_token": "",
|
||||
"git_bridge_modal_you_can_also_git_clone": "",
|
||||
"git_integration": "",
|
||||
"git_integration_info": "",
|
||||
"github_commit_message_placeholder": "",
|
||||
|
|
|
@ -31,11 +31,11 @@ function LinkingSection() {
|
|||
path: string
|
||||
}[]
|
||||
|
||||
const showPersonalAccessToken = getMeta(
|
||||
'ol-showPersonalAccessToken'
|
||||
) as boolean
|
||||
const showPersonalAccessTokenComponents: boolean =
|
||||
getMeta('ol-showPersonalAccessToken') ||
|
||||
getMeta('ol-optionalPersonalAccessToken')
|
||||
|
||||
const allIntegrationLinkingWidgets = showPersonalAccessToken
|
||||
const allIntegrationLinkingWidgets = showPersonalAccessTokenComponents
|
||||
? integrationLinkingWidgets.concat(oauth2ServerComponents)
|
||||
: integrationLinkingWidgets
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import Tooltip from '../../../shared/components/tooltip'
|
|||
import Icon from '../../../shared/components/icon'
|
||||
import { useShareProjectContext } from './share-project-modal'
|
||||
import { setProjectAccessLevel } from '../utils/api'
|
||||
import CopyLink from '../../../shared/components/copy-link'
|
||||
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
|
@ -265,10 +265,10 @@ function AccessToken({ token, path, tooltipId }) {
|
|||
const link = `${origin}${path}${token}`
|
||||
|
||||
return (
|
||||
<pre className="access-token">
|
||||
<span>{link}</span>
|
||||
<CopyLink link={link} tooltipId={tooltipId} />
|
||||
</pre>
|
||||
<div className="access-token">
|
||||
<code>{link}</code>
|
||||
<CopyToClipboard content={link} tooltipId={tooltipId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Tooltip from './tooltip'
|
||||
import Icon from './icon'
|
||||
|
||||
type CopyLinkProps = {
|
||||
link: string
|
||||
tooltipId: string
|
||||
}
|
||||
|
||||
function CopyLink({ link, tooltipId }: CopyLinkProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
setCopied(true)
|
||||
window.setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
})
|
||||
}, [link])
|
||||
|
||||
if (!navigator.clipboard?.writeText) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id={tooltipId}
|
||||
description={copied ? 'Copied!' : <Trans i18nKey="copy" />}
|
||||
overlayProps={{
|
||||
delayHide: copied ? 1000 : 250,
|
||||
shouldUpdatePosition: true,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
bsSize="xsmall"
|
||||
bsStyle="link"
|
||||
className="copy-button"
|
||||
aria-label={t('copy')}
|
||||
>
|
||||
{copied ? <Icon type="check" /> : <Icon type="clipboard" />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyLink
|
|
@ -0,0 +1,84 @@
|
|||
import { FC, memo, MouseEventHandler, useCallback, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from './tooltip'
|
||||
import Icon from './icon'
|
||||
|
||||
export const CopyToClipboard = memo<{
|
||||
content: string
|
||||
tooltipId: string
|
||||
kind?: 'text' | 'icon'
|
||||
}>(({ content, tooltipId, kind = 'icon' }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
setCopied(true)
|
||||
window.setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
})
|
||||
}, [content])
|
||||
|
||||
if (!navigator.clipboard?.writeText) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id={tooltipId}
|
||||
description={copied ? `${t('copied')}!` : t('copy')}
|
||||
overlayProps={{
|
||||
delayHide: copied ? 1000 : 250,
|
||||
shouldUpdatePosition: true,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{kind === 'text' ? (
|
||||
<TextButton handleClick={handleClick} />
|
||||
) : (
|
||||
<IconButton handleClick={handleClick} copied={copied} />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
CopyToClipboard.displayName = 'CopyToClipboard'
|
||||
|
||||
const TextButton: FC<{
|
||||
handleClick: MouseEventHandler<Button>
|
||||
}> = ({ handleClick }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
bsSize="xsmall"
|
||||
bsStyle="secondary"
|
||||
className="copy-button"
|
||||
>
|
||||
{t('copy')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const IconButton: FC<{
|
||||
handleClick: MouseEventHandler<Button>
|
||||
copied: boolean
|
||||
}> = ({ handleClick, copied }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
bsSize="xsmall"
|
||||
bsStyle="link"
|
||||
className="copy-button"
|
||||
aria-label={t('copy')}
|
||||
>
|
||||
<Icon type={copied ? 'check' : 'clipboard'} />
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
mockCompileError,
|
||||
} from '../fixtures/compile'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { useMeta } from '../hooks/use-meta'
|
||||
|
||||
const scopeWatchers: [string, (value: any) => void][] = []
|
||||
|
||||
|
@ -210,7 +211,8 @@ type ScopeDecoratorOptions = {
|
|||
|
||||
export const ScopeDecorator = (
|
||||
Story: any,
|
||||
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true }
|
||||
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true },
|
||||
meta: Record<string, any> = {}
|
||||
) => {
|
||||
// mock compile on load
|
||||
useFetchMock(fetchMock => {
|
||||
|
@ -232,6 +234,9 @@ export const ScopeDecorator = (
|
|||
return initialize()
|
||||
}, [])
|
||||
|
||||
// set values on window.metaAttributesCache (created in initialize, above)
|
||||
useMeta(meta)
|
||||
|
||||
return (
|
||||
<ContextRoot ide={ide}>
|
||||
<Story />
|
||||
|
|
|
@ -117,6 +117,6 @@
|
|||
// TODO: find a way for modules to add styles dynamically
|
||||
@import 'modules/symbol-palette.less';
|
||||
@import 'modules/managed-users.less';
|
||||
|
||||
@import 'modules/git-bridge-modal.less';
|
||||
@import 'modules/admin-panel.less';
|
||||
@import 'modules/overleaf-integration.less';
|
||||
|
|
|
@ -148,5 +148,6 @@
|
|||
// TODO: find a way for modules to add styles dynamically
|
||||
@import 'modules/symbol-palette.less';
|
||||
@import 'modules/admin-panel.less';
|
||||
@import 'modules/git-bridge-modal.less';
|
||||
@import 'modules/managed-users.less';
|
||||
@import 'modules/overleaf-integration.less';
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
.git-bridge-copy {
|
||||
background: @neutral-10;
|
||||
color: @neutral-90;
|
||||
padding: @padding-sm;
|
||||
border-radius: @border-radius-large;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: @padding-sm;
|
||||
align-items: center;
|
||||
margin: @margin-lg 0;
|
||||
|
||||
& code {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.git-bridge-optional-tokens {
|
||||
border: 1px solid @gray-lightest;
|
||||
border-radius: @border-radius-large;
|
||||
padding: @padding-md;
|
||||
margin: @margin-lg 0;
|
||||
}
|
||||
|
||||
.git-bridge-optional-tokens-header {
|
||||
font-family: @font-family-sans-serif;
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
margin-bottom: @margin-sm;
|
||||
}
|
||||
|
||||
.git-bridge-optional-tokens-actions {
|
||||
margin-top: @margin-sm;
|
||||
}
|
|
@ -577,8 +577,8 @@
|
|||
"git_authentication_token": "Git autentificeringsnøgle",
|
||||
"git_authentication_token_create_modal_info_1": "Dette er din Git autentificeringsnøgle. Du skal indtaste den når du bliver spurgt om et kodeord.",
|
||||
"git_authentication_token_create_modal_info_2": "<0>Du kan kun se denne autentificeringsnøgle én gang så kopier den venligst og opbevar den sikkert.</0> For flere instruktioner omkring brugen af autentificeringsnøgler, besøg vores <1>hjælpeside</1>.",
|
||||
"git_bridge_modal_click_generate": "Klik “Generér nøgle” for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
|
||||
"git_bridge_modal_description": "Du kan <code>git</code> <code>clone</code> dit projekt med linket herunder.",
|
||||
"git_bridge_modal_click_generate": "Klik <strong>“Generér nøgle”</strong> for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
|
||||
"git_bridge_modal_description": "Du kan git clone dit projekt med linket herunder.",
|
||||
"git_bridge_modal_enter_authentication_token": "Når du bliver spurgt om en kode, indtast da din nye autentificeringsnøgle:",
|
||||
"git_bridge_modal_see_once": "Du kan kun se denne autentificeringsnøgle én gang. For at slette den eller generere en ny, gå til dine brugerindstilinger. For detalerede instruktioner og fejlsøgning, læs vores <0>hjælpeside</0>.",
|
||||
"git_bridge_modal_tokens_description": "For at git clone dit projekt har du brug for linket herunder og en git autentificeringsnøgle.",
|
||||
|
|
|
@ -582,8 +582,8 @@
|
|||
"git_authentication_token": "Git Anmeldungs-Token",
|
||||
"git_authentication_token_create_modal_info_1": "Das ist dein Git Anmeldungs-Token. Verwende ihn wenn Du nach einem Passwort gefragt wirst.",
|
||||
"git_authentication_token_create_modal_info_2": "<0>Du bekommst diesen Anmelde-Token nur einmal angezeigt, bitte kopiere ihn und bewahre ihn sicher auf.</0> Für weitere Anweisungen zur Verwendung von Anmelde-Tokens, besuche unsere <1>Hilfe-Seite</1>.",
|
||||
"git_bridge_modal_click_generate": "Klicke jetzt auf Token generieren um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
|
||||
"git_bridge_modal_description": "Du kannst <code>git</code> <code>clone</code> für dein Projekt über den unten angezeigten Link ausführen.",
|
||||
"git_bridge_modal_click_generate": "Klicke jetzt auf <strong>Token generieren</strong> um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
|
||||
"git_bridge_modal_description": "Du kannst git clone für dein Projekt über den unten angezeigten Link ausführen.",
|
||||
"git_bridge_modal_enter_authentication_token": "Wenn Du nach einem Passwort gefragt wirst, gib deinen neuen Anmeldungs-Token ein:",
|
||||
"git_bridge_modal_see_once": "Du siehst diesen Token nur einmal. Um ihn zu löschen oder einen weiteren zu generieren, besuche die Kontoeinstellungen. Für detaillierte Anweisungen und Problembehebung, besuche unsere <0>Hilfe-Seite</0>.",
|
||||
"git_bridge_modal_tokens_description": "Um einen Git-Clone deines Projekts zu erstellen, benutze folgenden Link und einen Git-Anmeldungs-Token.",
|
||||
|
|
|
@ -647,12 +647,14 @@
|
|||
"git_authentication_token": "Git authentication token",
|
||||
"git_authentication_token_create_modal_info_1": "This is your Git authentication token. You should enter this when prompted for a password.",
|
||||
"git_authentication_token_create_modal_info_2": "<0>You will only see this authentication token once so please copy it and keep it safe</0>. For full instructions on using authentication tokens, visit our <1>help page</1>.",
|
||||
"git_bridge_modal_click_generate": "Click Generate token to generate your authentication token now. Or do this later in your Account Settings.",
|
||||
"git_bridge_modal_description": "You can <code>git</code> <code>clone</code> your project using the link displayed below.",
|
||||
"git_bridge_modal_click_generate": "Click <strong>Generate token</strong> to generate your authentication token now. Or do this later in your Account Settings.",
|
||||
"git_bridge_modal_description": "You can git clone your project using the link displayed below.",
|
||||
"git_bridge_modal_enter_authentication_token": "When prompted for a password, enter your new authentication token:",
|
||||
"git_bridge_modal_git_authentication_tokens": "Git authentication tokens",
|
||||
"git_bridge_modal_see_once": "You’ll only see this token once. To delete it or generate a new one, visit Account Settings. For detailed instructions and troubleshooting, read our <0>help page</0>.",
|
||||
"git_bridge_modal_tokens_description": "To git clone your project you’ll need the link below and a Git authentication token.",
|
||||
"git_bridge_modal_use_previous_token": "If you’re prompted for a password, you can use a previously generated Git authentication token. Or you can generate a new one in Account Settings. For more support, read our <0>help page</0>.",
|
||||
"git_bridge_modal_you_can_also_git_clone": "You can also git clone your project by using the link below and a Git authentication token.",
|
||||
"git_integration": "Git Integration",
|
||||
"git_integration_info": "With Git integration, you can clone your Overleaf projects with Git. For full instructions on how to do this, read <0>our help page</0>.",
|
||||
"git_integration_lowercase": "Git integration",
|
||||
|
|
|
@ -364,7 +364,7 @@
|
|||
"get_same_latex_setup": "Avec __appName__, votre configuration LaTeX vous suit partout. En travaillant avec vos collègues ou étudiant·e·s sur __appName__, vous êtes sûr·e de ne pas rencontrer d’incohérences de version ou de conflits de paquets.",
|
||||
"get_started_now": "Commencer maintenant",
|
||||
"git": "Git",
|
||||
"git_bridge_modal_description": "Vous pouvez effectuer un <code>git</code> <code>clone</code> de votre projet en utilisant le lien ci-dessous.",
|
||||
"git_bridge_modal_description": "Vous pouvez effectuer un git clone de votre projet en utilisant le lien ci-dessous.",
|
||||
"github_commit_message_placeholder": "Message de commit pour les changements effectués dans __appName__…",
|
||||
"github_credentials_expired": "Vos identifiants GitHub ont expiré",
|
||||
"github_for_link_shared_projects": "Vous avez accédé à ce projet par un partage de lien : celui-ci ne sera pas synchronisé à votre GitHub tant que vous n’aurez pas été invité par courriel par le propriétaire du projet.",
|
||||
|
|
|
@ -261,7 +261,7 @@
|
|||
"get_same_latex_setup": "Com __appName__ você tem o mesmo aplicativo onde quer que vá. Trabalhando com seus colegas e estudantes no __appName__, você sabe que não terá problema de inconsistência ou conflito de pacotes.",
|
||||
"get_started_now": "Comece agora",
|
||||
"git": "Git",
|
||||
"git_bridge_modal_description": "Você pode usar <code>git clone</code> no seu projeto usando o link abaixo.",
|
||||
"git_bridge_modal_description": "Você pode usar git clone no seu projeto usando o link abaixo.",
|
||||
"github_commit_message_placeholder": "Mensagem de commit para as alterações feitas no __appName__...",
|
||||
"github_credentials_expired": "Suas credenciais de autorização do GitHub expiraram",
|
||||
"github_for_link_shared_projects": "Este projeto foi acessado por meio do compartilhamento de links e não será sincronizado com o seu Github, a menos que você seja convidado por e-mail pelo proprietário do projeto.",
|
||||
|
|
|
@ -362,7 +362,7 @@
|
|||
"get_same_latex_setup": "您到任何地方都可以用 __appName__ 实现LaTeX的功能。由于您和您的同事和学生可以在 __appName__ 上共同工作,不会出现版本不一致和包冲突的情况。",
|
||||
"get_started_now": "立即开始",
|
||||
"git": "Git",
|
||||
"git_bridge_modal_description": "您可以使用下面显示的链接来<code>git</code><code>clone</code>您的项目。",
|
||||
"git_bridge_modal_description": "您可以使用下面显示的链接来git clone您的项目。",
|
||||
"github_commit_message_placeholder": "为 __appName__ 中的更改提交信息",
|
||||
"github_credentials_expired": "您的 Github 授权凭证已过期",
|
||||
"github_for_link_shared_projects": "此项目是通过链接共享访问的,除非项目所有者通过电子邮件邀请您,否则不会与您的Github同步。",
|
||||
|
|
|
@ -82,6 +82,11 @@ describe('UserPagesController', function () {
|
|||
getAdminEmail: sinon.stub().returns(this.adminEmail),
|
||||
},
|
||||
}
|
||||
this.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignment: sinon.stub().returns('default'),
|
||||
},
|
||||
}
|
||||
this.UserPagesController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.settings,
|
||||
|
@ -96,6 +101,7 @@ describe('UserPagesController', function () {
|
|||
'../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager':
|
||||
this.PersonalAccessTokenManager,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||
request: (this.request = sinon.stub()),
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue