From e15e495515621830a1002e27b0ecc61f898d55ae Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:07:21 +0200 Subject: [PATCH 01/30] feat: Add a Connected Account Setting for Pull Requests --- .../Nav/Settings/ConnectedAccounts.tsx | 267 ++++++++++++++++++ .../__tests__/ConnectedAccounts.spec.tsx | 216 ++++++++++++++ .../Nav/Settings/__tests__/Sidebar.spec.tsx | 1 + .../Nav/Settings/__tests__/registry.spec.ts | 1 + .../src/components/Nav/Settings/context.tsx | 3 + .../src/components/Nav/Settings/registry.tsx | 11 + client/src/components/Nav/Settings/types.ts | 3 + .../PullRequest/connection.test.tsx | 96 +++++++ .../data-provider/PullRequest/connection.ts | 59 ++++ client/src/data-provider/PullRequest/index.ts | 1 + client/src/locales/en/translation.json | 19 ++ packages/data-provider/src/api-endpoints.ts | 8 + packages/data-provider/src/data-service.ts | 12 + packages/data-provider/src/keys.ts | 3 + 14 files changed, 700 insertions(+) create mode 100644 client/src/components/Nav/Settings/ConnectedAccounts.tsx create mode 100644 client/src/components/Nav/Settings/__tests__/ConnectedAccounts.spec.tsx create mode 100644 client/src/data-provider/PullRequest/connection.test.tsx create mode 100644 client/src/data-provider/PullRequest/connection.ts diff --git a/client/src/components/Nav/Settings/ConnectedAccounts.tsx b/client/src/components/Nav/Settings/ConnectedAccounts.tsx new file mode 100644 index 00000000000..393f3fd46e8 --- /dev/null +++ b/client/src/components/Nav/Settings/ConnectedAccounts.tsx @@ -0,0 +1,267 @@ +import { useCallback, useId, useRef, useState } from 'react'; +import { ExternalLink, Gitlab } from 'lucide-react'; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, + Chip, + GithubIcon, + Skeleton, + Spinner, +} from '@librechat/client'; +import type { PullRequestProvider } from 'librechat-data-provider'; +import type { ComponentType } from 'react'; +import type { TranslationKeys } from '~/hooks'; +import { + useGetStartupConfig, + usePullRequestConnectionQuery, + useConnectPullRequestAccountMutation, + useDisconnectPullRequestAccountMutation, +} from '~/data-provider'; +import { useLocalize } from '~/hooks'; + +const PROVIDERS: Record< + PullRequestProvider, + { Icon: ComponentType<{ className?: string }>; name: TranslationKeys } +> = { + github: { Icon: GithubIcon, name: 'com_ui_pr_provider_github' }, + gitlab: { Icon: Gitlab, name: 'com_ui_pr_provider_gitlab' }, + bitbucket: { Icon: ExternalLink, name: 'com_ui_pr_provider_bitbucket' }, +}; + +type ConnectError = 'com_ui_pr_account_connect_error' | 'com_ui_pr_account_popup_blocked'; + +/** + * The user's own account for pull request lookups. One row for the provider the operator + * configured: the status, and a single action to connect or disconnect it. The action button is + * swapped as the state changes, so focus is carried over to whichever button replaces it. + */ +export default function ConnectedAccounts() { + const localize = useLocalize(); + const rowId = useId(); + const { data: startupConfig } = useGetStartupConfig(); + const status = usePullRequestConnectionQuery(); + const connectMutation = useConnectPullRequestAccountMutation(); + const disconnectMutation = useDisconnectPullRequestAccountMutation(); + const [confirmOpen, setConfirmOpen] = useState(false); + const [connectError, setConnectError] = useState(null); + const [disconnectFailed, setDisconnectFailed] = useState(false); + const actionRef = useRef(null); + const keepFocus = useRef(false); + + /** + * Callback ref, so a replacement button takes focus as it mounts after an action, but only + * when the button it replaced took focus away with it. + */ + const bindAction = useCallback((button: HTMLButtonElement | null) => { + actionRef.current = button; + if (button == null || !keepFocus.current) { + return; + } + keepFocus.current = false; + if (document.activeElement == null || document.activeElement === document.body) { + button.focus(); + } + }, []); + + const provider = startupConfig?.pullRequestsConnect; + /** The server is the authority: an app it no longer has is nothing to connect to. */ + if (provider == null || status.data?.configured === false) { + return null; + } + + const { Icon, name: nameKey } = PROVIDERS[provider]; + const name = localize(nameKey); + const labelId = `${rowId}-label`; + const descriptionId = `${rowId}-description`; + + const connect = () => { + if (connectMutation.isLoading) { + return; + } + setConnectError(null); + /** Opened inside the click so the browser treats it as user initiated. */ + const popup = window.open('', '_blank'); + if (popup == null) { + setConnectError('com_ui_pr_account_popup_blocked'); + return; + } + popup.opener = null; + connectMutation.mutate(undefined, { + onSuccess: ({ url }) => { + keepFocus.current = true; + popup.location.href = url; + }, + onError: () => { + popup.close(); + setConnectError('com_ui_pr_account_connect_error'); + }, + }); + }; + + const disconnect = () => { + setDisconnectFailed(false); + disconnectMutation.mutate(undefined, { + onSuccess: () => { + keepFocus.current = true; + setConfirmOpen(false); + }, + onError: () => setDisconnectFailed(true), + }); + }; + + const retry = () => { + keepFocus.current = true; + status.refetch().then((result) => { + if (result.isError) { + keepFocus.current = false; + } + }); + }; + + let statusContent = {localize('com_ui_pr_account_checking')}; + let action =