diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js
index 6c59b47931..a05fcf6078 100644
--- a/services/web/app/src/Features/Project/ProjectController.js
+++ b/services/web/app/src/Features/Project/ProjectController.js
@@ -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,
})
diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js
index 28dbbdac87..8e96000d5e 100644
--- a/services/web/app/src/Features/User/UserPagesController.js
+++ b/services/web/app/src/Features/User/UserPagesController.js
@@ -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,
diff --git a/services/web/app/views/project/editor/meta.pug b/services/web/app/views/project/editor/meta.pug
index fcadeb204d..005027bfa3 100644
--- a/services/web/app/views/project/editor/meta.pug
+++ b/services/web/app/views/project/editor/meta.pug
@@ -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)
diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug
index 65e6a8b5dc..e5d8582095 100644
--- a/services/web/app/views/user/settings.pug
+++ b/services/web/app/views/user/settings.pug
@@ -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)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 6fa147eb0a..9f0631c583 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -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": "",
diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx
index 250fb9eef1..0d02a5c250 100644
--- a/services/web/frontend/js/features/settings/components/linking-section.tsx
+++ b/services/web/frontend/js/features/settings/components/linking-section.tsx
@@ -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
diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx b/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
index 13dda1e38e..c7a9c1e552 100644
--- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
@@ -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 (
-
- {link}
-
-
+
+ {link}
+
+
)
}
diff --git a/services/web/frontend/js/shared/components/copy-link.tsx b/services/web/frontend/js/shared/components/copy-link.tsx
deleted file mode 100644
index 96603a0beb..0000000000
--- a/services/web/frontend/js/shared/components/copy-link.tsx
+++ /dev/null
@@ -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 (
- }
- overlayProps={{
- delayHide: copied ? 1000 : 250,
- shouldUpdatePosition: true,
- }}
- >
-
-
- )
-}
-
-export default CopyLink
diff --git a/services/web/frontend/js/shared/components/copy-to-clipboard.tsx b/services/web/frontend/js/shared/components/copy-to-clipboard.tsx
new file mode 100644
index 0000000000..af22f0fa1d
--- /dev/null
+++ b/services/web/frontend/js/shared/components/copy-to-clipboard.tsx
@@ -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 (
+
+
+ {kind === 'text' ? (
+
+ ) : (
+
+ )}
+
+
+ )
+})
+CopyToClipboard.displayName = 'CopyToClipboard'
+
+const TextButton: FC<{
+ handleClick: MouseEventHandler