From a42eec5c9ca34417e46b6fe68b4c37ef9bb2ba4a Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Tue, 11 Aug 2026 23:40:55 +0000 Subject: [PATCH] feat(ui): add GitLab connect settings --- .../sections/git-identities/GitPage.tsx | 2 + .../sections/openchamber/GitLabSettings.tsx | 293 ++++++++++++++++++ .../ui/src/lib/i18n/messages/de.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/en.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/es.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/fr.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/ja.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/ko.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/pl.settings.ts | 23 ++ .../src/lib/i18n/messages/pt-BR.settings.ts | 23 ++ .../ui/src/lib/i18n/messages/uk.settings.ts | 23 ++ .../src/lib/i18n/messages/zh-CN.settings.ts | 23 ++ .../src/lib/i18n/messages/zh-TW.settings.ts | 23 ++ packages/ui/src/lib/settings/search.ts | 6 + packages/ui/src/stores/useGitLabAuthStore.ts | 72 +++++ 15 files changed, 626 insertions(+) create mode 100644 packages/ui/src/components/sections/openchamber/GitLabSettings.tsx create mode 100644 packages/ui/src/stores/useGitLabAuthStore.ts diff --git a/packages/ui/src/components/sections/git-identities/GitPage.tsx b/packages/ui/src/components/sections/git-identities/GitPage.tsx index d90d1a8f..8d789a2e 100644 --- a/packages/ui/src/components/sections/git-identities/GitPage.tsx +++ b/packages/ui/src/components/sections/git-identities/GitPage.tsx @@ -20,6 +20,7 @@ import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCrede import { useShallow } from 'zustand/react/shallow'; import { GitSettings } from '@/components/sections/openchamber/GitSettings'; import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings'; +import { GitLabSettings } from '@/components/sections/openchamber/GitLabSettings'; import { GitIdentityEditorDialog } from './GitIdentityEditorDialog'; import { Icon } from "@/components/icon/Icon"; import type { IconName } from "@/components/icon/icons"; @@ -122,6 +123,7 @@ export const GitPage: React.FC = () => { showSaveStatus > + { + if (!baseUrl) return ''; + try { + return new URL(baseUrl).host; + } catch { + return baseUrl; + } +}; + +export const GitLabSettings: React.FC = () => { + const { t } = useI18n(); + const { isMobile } = useDeviceInfo(); + const runtimeGitLab = getRegisteredRuntimeAPIs()?.gitlab; + const status = useGitLabAuthStore((state) => state.status); + const isLoading = useGitLabAuthStore((state) => state.isLoading); + const hasChecked = useGitLabAuthStore((state) => state.hasChecked); + const refreshStatus = useGitLabAuthStore((state) => state.refreshStatus); + const setStatus = useGitLabAuthStore((state) => state.setStatus); + + const [isBusy, setIsBusy] = React.useState(false); + const [accessToken, setAccessToken] = React.useState(''); + const [baseUrl, setBaseUrl] = React.useState(''); + + React.useEffect(() => { + (async () => { + try { + if (!hasChecked) { + await refreshStatus(runtimeGitLab); + } + } catch (error) { + console.warn('Failed to load GitLab auth status:', error); + } + })(); + }, [hasChecked, refreshStatus, runtimeGitLab]); + + const connect = React.useCallback(async () => { + const trimmedToken = accessToken.trim(); + if (!trimmedToken) { + toast.error(t('settings.gitlab.page.errors.invalidToken')); + return; + } + const trimmedBaseUrl = baseUrl.trim() || undefined; + setIsBusy(true); + try { + const payload = runtimeGitLab + ? await runtimeGitLab.authConnect({ accessToken: trimmedToken, baseUrl: trimmedBaseUrl }) + : await (async () => { + const response = await runtimeFetch('/api/gitlab/auth/connect', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ accessToken: trimmedToken, baseUrl: trimmedBaseUrl }), + }); + const body = (await response.json().catch(() => null)) as GitLabAuthStatus | { error?: string } | null; + if (!response.ok || !body) { + throw new Error((body as { error?: string } | null)?.error || response.statusText); + } + return body as GitLabAuthStatus; + })(); + + setStatus(payload); + setAccessToken(''); + setBaseUrl(''); + toast.success(t('settings.gitlab.page.toast.connected')); + } catch (error) { + console.error('Failed to connect GitLab:', error); + toast.error(t('settings.gitlab.page.errors.failed')); + } finally { + setIsBusy(false); + } + }, [accessToken, baseUrl, runtimeGitLab, setStatus, t]); + + const disconnect = React.useCallback(async () => { + setIsBusy(true); + try { + if (runtimeGitLab) { + await runtimeGitLab.authDisconnect(); + } else { + const response = await runtimeFetch('/api/gitlab/auth', { + method: 'DELETE', + headers: { Accept: 'application/json' }, + }); + if (!response.ok) { + throw new Error(response.statusText); + } + } + toast.success(t('settings.gitlab.page.toast.disconnected')); + await refreshStatus(runtimeGitLab, { force: true }); + } catch (error) { + console.error('Failed to disconnect GitLab:', error); + toast.error(t('settings.gitlab.page.toast.disconnectFailed')); + } finally { + setIsBusy(false); + } + }, [refreshStatus, runtimeGitLab, t]); + + const activateAccount = React.useCallback(async (accountId: string) => { + if (!accountId) return; + setIsBusy(true); + try { + const payload = runtimeGitLab + ? await runtimeGitLab.authActivate(accountId) + : await (async () => { + const response = await runtimeFetch('/api/gitlab/auth/activate', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ accountId }), + }); + const body = (await response.json().catch(() => null)) as GitLabAuthStatus | { error?: string } | null; + if (!response.ok || !body) { + throw new Error((body as { error?: string } | null)?.error || response.statusText); + } + return body as GitLabAuthStatus; + })(); + + setStatus(payload); + toast.success(t('settings.gitlab.page.toast.accountSwitched')); + } catch (error) { + console.error('Failed to switch GitLab account:', error); + toast.error(t('settings.gitlab.page.toast.accountSwitchFailed')); + } finally { + setIsBusy(false); + } + }, [runtimeGitLab, setStatus, t]); + + if (isLoading) { + return null; + } + + const connected = Boolean(status?.connected); + const user = status?.user; + const accounts = status?.accounts ?? []; + const otherAccounts = accounts.filter((account) => !account.current); + const currentAccount = accounts.find((account) => account.current) ?? (accounts.length > 0 ? accounts[0] : null); + const currentBaseUrlHost = getBaseUrlHost(currentAccount?.baseUrl ?? status?.defaultBaseUrl); + + return ( + +
+ {connected ? ( +
+
+ {user?.avatarUrl ? ( + {user.username + ) : ( +
+ +
+ )} + +
+
+ {user?.name?.trim() || user?.username || 'GitLab'} +
+
+ + {t('settings.gitlab.page.connectedAs')} + {user?.username || t('settings.gitlab.page.label.unknownUser')} + + {currentBaseUrlHost} +
+
+
+ + +
+ ) : ( +
+
+ + setAccessToken(event.target.value)} + placeholder={t('settings.gitlab.page.accessToken.placeholder')} + className="h-9 max-w-[24rem]" + /> +
+
+ + setBaseUrl(event.target.value)} + placeholder={t('settings.gitlab.page.baseUrl.placeholder')} + className="h-9 max-w-[24rem]" + /> +
+
+ {t('settings.gitlab.page.status.notConnected')} + +
+
+ )} + + {otherAccounts.length > 0 && ( +
+
+ {t('settings.gitlab.page.label.otherAccounts')} +
+
+ {otherAccounts.map((account) => { + const accountUser = account.user; + return ( +
+
+ {accountUser?.avatarUrl ? ( + {accountUser.username + ) : ( +
+ +
+ )} +
+ + {accountUser?.name?.trim() || accountUser?.username || 'GitLab'} + + {accountUser?.username && ( + + {accountUser.username} + · + {getBaseUrlHost(account.baseUrl)} + + )} +
+
+ +
+ ); + })} +
+
+ )} +
+
+ ); +}; diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index 8c3ec09e..fc8d9a01 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1629,6 +1629,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI Fallback aktiviert', 'settings.github.page.toast.ghCliDisabled': 'gh CLI Fallback deaktiviert', 'settings.github.page.toast.ghCliUpdateFailed': 'Fehler beim Aktualisieren der gh CLI Einstellung', + 'settings.gitlab.page.title': 'GitLab Personal Access Token', + 'settings.gitlab.page.description': 'Fügen Sie ein GitLab Personal Access Token ein, um eine Verbindung herzustellen. Legen Sie die Basis-URL fest, wenn Sie eine selbst gehostete GitLab-Instanz verwenden.', + 'settings.gitlab.page.tooltip.connectAccount': 'Verbinden Sie ein GitLab-Konto für Issue- und Merge-Request-Workflows in der App.', + 'settings.gitlab.page.accessToken.label': 'Personal Access Token', + 'settings.gitlab.page.accessToken.placeholder': 'Fügen Sie Ihr GitLab Personal Access Token ein', + 'settings.gitlab.page.baseUrl.label': 'Basis-URL (optional)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'GitLab verbinden', + 'settings.gitlab.page.actions.disconnect': 'Trennen', + 'settings.gitlab.page.actions.switch': 'Wechseln zu', + 'settings.gitlab.page.status.notConnected': 'Nicht verbunden', + 'settings.gitlab.page.label.unknownUser': 'unbekannt', + 'settings.gitlab.page.label.otherAccounts': 'Andere Konten', + 'settings.gitlab.page.avatarAlt.withLogin': 'Avatar von {login}', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab-Avatar', + 'settings.gitlab.page.connectedAs': 'Verbunden als', + 'settings.gitlab.page.errors.invalidToken': 'Geben Sie ein gültiges GitLab Personal Access Token ein', + 'settings.gitlab.page.errors.failed': 'Verbindung zu GitLab fehlgeschlagen', + 'settings.gitlab.page.toast.connected': 'GitLab verbunden', + 'settings.gitlab.page.toast.disconnected': 'GitLab getrennt', + 'settings.gitlab.page.toast.disconnectFailed': 'Trennen von GitLab fehlgeschlagen', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab-Konto gewechselt', + 'settings.gitlab.page.toast.accountSwitchFailed': 'Wechsel des GitLab-Kontos fehlgeschlagen', 'settings.notifications.page.delivery.title': 'Benachrichtigungsübermittlung', 'settings.notifications.page.delivery.enableAria': 'Benachrichtigungen aktivieren', 'settings.notifications.page.delivery.enableLabel': 'Benachrichtigungen aktivieren', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 66bea849..3d854ef7 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1695,6 +1695,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI fallback enabled', 'settings.github.page.toast.ghCliDisabled': 'gh CLI fallback disabled', 'settings.github.page.toast.ghCliUpdateFailed': 'Failed to update gh CLI setting', + 'settings.gitlab.page.title': 'GitLab Personal Access Token', + 'settings.gitlab.page.description': 'Paste a GitLab personal access token to connect. Set the base URL when using a self-hosted GitLab instance.', + 'settings.gitlab.page.tooltip.connectAccount': 'Connect a GitLab account for in-app issue and merge request workflows.', + 'settings.gitlab.page.accessToken.label': 'Personal Access Token', + 'settings.gitlab.page.accessToken.placeholder': 'Paste your GitLab personal access token', + 'settings.gitlab.page.baseUrl.label': 'Base URL (optional)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'Connect GitLab', + 'settings.gitlab.page.actions.disconnect': 'Disconnect', + 'settings.gitlab.page.actions.switch': 'Switch to', + 'settings.gitlab.page.status.notConnected': 'Not Connected', + 'settings.gitlab.page.label.unknownUser': 'unknown', + 'settings.gitlab.page.label.otherAccounts': 'Other Accounts', + 'settings.gitlab.page.avatarAlt.withLogin': '{login} avatar', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab avatar', + 'settings.gitlab.page.connectedAs': 'Connected as', + 'settings.gitlab.page.errors.invalidToken': 'Enter a valid GitLab personal access token', + 'settings.gitlab.page.errors.failed': 'Failed to connect GitLab', + 'settings.gitlab.page.toast.connected': 'GitLab connected', + 'settings.gitlab.page.toast.disconnected': 'GitLab disconnected', + 'settings.gitlab.page.toast.disconnectFailed': 'Failed to disconnect GitLab', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab account switched', + 'settings.gitlab.page.toast.accountSwitchFailed': 'Failed to switch GitLab account', 'settings.notifications.page.delivery.title': 'Notification Delivery', 'settings.notifications.page.delivery.enableAria': 'Enable notifications', 'settings.notifications.page.delivery.enableLabel': 'Enable Notifications', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 9b4d3094..1dcd4f51 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1672,6 +1672,29 @@ export const settingsDict = { "settings.github.page.toast.ghCliEnabled": "Respaldo de gh CLI activado", "settings.github.page.toast.ghCliDisabled": "Respaldo de gh CLI desactivado", "settings.github.page.toast.ghCliUpdateFailed": "No se pudo actualizar la configuración de gh CLI", + "settings.gitlab.page.title": "Token de acceso personal de GitLab", + "settings.gitlab.page.description": "Pega un token de acceso personal de GitLab para conectarte. Establece la URL base cuando uses una instancia de GitLab autoalojada.", + "settings.gitlab.page.tooltip.connectAccount": "Conecta una cuenta de GitLab para los flujos de trabajo de issues y merge requests en la aplicación.", + "settings.gitlab.page.accessToken.label": "Token de acceso personal", + "settings.gitlab.page.accessToken.placeholder": "Pega tu token de acceso personal de GitLab", + "settings.gitlab.page.baseUrl.label": "URL base (opcional)", + "settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com", + "settings.gitlab.page.actions.connect": "Conectar GitLab", + "settings.gitlab.page.actions.disconnect": "Desconectar", + "settings.gitlab.page.actions.switch": "Cambiar a", + "settings.gitlab.page.status.notConnected": "No conectado", + "settings.gitlab.page.label.unknownUser": "desconocido", + "settings.gitlab.page.label.otherAccounts": "Otras cuentas", + "settings.gitlab.page.avatarAlt.withLogin": "Avatar de {login}", + "settings.gitlab.page.avatarAlt.fallback": "Avatar de GitLab", + "settings.gitlab.page.connectedAs": "Conectado como", + "settings.gitlab.page.errors.invalidToken": "Introduce un token de acceso personal de GitLab válido", + "settings.gitlab.page.errors.failed": "No se pudo conectar GitLab", + "settings.gitlab.page.toast.connected": "GitLab conectado", + "settings.gitlab.page.toast.disconnected": "GitLab desconectado", + "settings.gitlab.page.toast.disconnectFailed": "No se pudo desconectar GitLab", + "settings.gitlab.page.toast.accountSwitched": "Cuenta de GitLab cambiada", + "settings.gitlab.page.toast.accountSwitchFailed": "No se pudo cambiar la cuenta de GitLab", "settings.notifications.page.delivery.title": "Entrega de notificaciones", "settings.notifications.page.delivery.enableAria": "Habilitar notificaciones", "settings.notifications.page.delivery.enableLabel": "Habilitar notificaciones", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 5e065403..ddb2627c 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1590,6 +1590,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'Solution de secours gh CLI activée', 'settings.github.page.toast.ghCliDisabled': 'Solution de secours gh CLI désactivée', 'settings.github.page.toast.ghCliUpdateFailed': 'Échec de la mise à jour du paramètre gh CLI', + 'settings.gitlab.page.title': 'Jeton d\'accès personnel GitLab', + 'settings.gitlab.page.description': 'Collez un jeton d\'accès personnel GitLab pour vous connecter. Définissez l\'URL de base si vous utilisez une instance GitLab auto-hébergée.', + 'settings.gitlab.page.tooltip.connectAccount': 'Connectez un compte GitLab pour les workflows d\'issues et de merge requests dans l\'application.', + 'settings.gitlab.page.accessToken.label': 'Jeton d\'accès personnel', + 'settings.gitlab.page.accessToken.placeholder': 'Collez votre jeton d\'accès personnel GitLab', + 'settings.gitlab.page.baseUrl.label': 'URL de base (facultatif)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'Connecter GitLab', + 'settings.gitlab.page.actions.disconnect': 'Déconnecter', + 'settings.gitlab.page.actions.switch': 'Passer à', + 'settings.gitlab.page.status.notConnected': 'Non connecté', + 'settings.gitlab.page.label.unknownUser': 'inconnu', + 'settings.gitlab.page.label.otherAccounts': 'Autres comptes', + 'settings.gitlab.page.avatarAlt.withLogin': 'Avatar de {login}', + 'settings.gitlab.page.avatarAlt.fallback': 'Avatar GitLab', + 'settings.gitlab.page.connectedAs': 'Connecté en tant que', + 'settings.gitlab.page.errors.invalidToken': 'Saisissez un jeton d\'accès personnel GitLab valide', + 'settings.gitlab.page.errors.failed': 'Échec de la connexion à GitLab', + 'settings.gitlab.page.toast.connected': 'GitLab connecté', + 'settings.gitlab.page.toast.disconnected': 'GitLab déconnecté', + 'settings.gitlab.page.toast.disconnectFailed': 'Échec de la déconnexion du GitLab', + 'settings.gitlab.page.toast.accountSwitched': 'Le compte GitLab a changé', + 'settings.gitlab.page.toast.accountSwitchFailed': 'Échec du changement de compte GitLab', 'settings.notifications.page.delivery.title': 'Envoi des notifications', 'settings.notifications.page.delivery.enableAria': 'Activer les notifications', 'settings.notifications.page.delivery.enableLabel': 'Activer les notifications', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index b842fe8a..7f2c59d1 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1705,6 +1705,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI フォールバックを有効化しました', 'settings.github.page.toast.ghCliDisabled': 'gh CLI フォールバックを無効化しました', 'settings.github.page.toast.ghCliUpdateFailed': 'gh CLI 設定の更新に失敗しました', + 'settings.gitlab.page.title': 'GitLab パーソナルアクセストークン', + 'settings.gitlab.page.description': 'GitLab パーソナルアクセストークンを貼り付けて接続します。セルフホストの GitLab インスタンスを使用する場合はベース URL を設定してください。', + 'settings.gitlab.page.tooltip.connectAccount': 'アプリ内の Issue とマージリクエストのワークフロー用に GitLab アカウントを接続します。', + 'settings.gitlab.page.accessToken.label': 'パーソナルアクセストークン', + 'settings.gitlab.page.accessToken.placeholder': 'GitLab パーソナルアクセストークンを貼り付け', + 'settings.gitlab.page.baseUrl.label': 'ベース URL(任意)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'GitLab に接続', + 'settings.gitlab.page.actions.disconnect': '切断', + 'settings.gitlab.page.actions.switch': '切り替え', + 'settings.gitlab.page.status.notConnected': '未接続', + 'settings.gitlab.page.label.unknownUser': '不明', + 'settings.gitlab.page.label.otherAccounts': 'その他のアカウント', + 'settings.gitlab.page.avatarAlt.withLogin': '{login} のアバター', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab のアバター', + 'settings.gitlab.page.connectedAs': '接続アカウント:', + 'settings.gitlab.page.errors.invalidToken': '有効な GitLab パーソナルアクセストークンを入力してください', + 'settings.gitlab.page.errors.failed': 'GitLab に接続できませんでした', + 'settings.gitlab.page.toast.connected': 'GitLab に接続しました', + 'settings.gitlab.page.toast.disconnected': 'GitLab の接続を切断しました', + 'settings.gitlab.page.toast.disconnectFailed': 'GitLab の切断に失敗しました', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab アカウントを切り替えました', + 'settings.gitlab.page.toast.accountSwitchFailed': 'GitLab アカウントの切り替えに失敗しました', 'settings.notifications.page.delivery.title': '通知配信', 'settings.notifications.page.delivery.enableAria': '通知を有効化', 'settings.notifications.page.delivery.enableLabel': '通知を有効化', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index aae1fb17..cddd9f20 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1672,6 +1672,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI 대체 활성화됨', 'settings.github.page.toast.ghCliDisabled': 'gh CLI 대체 비활성화됨', 'settings.github.page.toast.ghCliUpdateFailed': 'gh CLI 설정을 업데이트하지 못했습니다', + 'settings.gitlab.page.title': 'GitLab 개인 액세스 토큰', + 'settings.gitlab.page.description': '연결하려면 GitLab 개인 액세스 토큰을 붙여넣으세요. 자체 호스팅 GitLab 인스턴스를 사용하는 경우 기본 URL을 설정하세요.', + 'settings.gitlab.page.tooltip.connectAccount': '앱 내 이슈 및 병합 요청 워크플로에 GitLab 계정을 연결합니다.', + 'settings.gitlab.page.accessToken.label': '개인 액세스 토큰', + 'settings.gitlab.page.accessToken.placeholder': 'GitLab 개인 액세스 토큰을 붙여넣으세요', + 'settings.gitlab.page.baseUrl.label': '기본 URL(선택 사항)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'GitLab 연결', + 'settings.gitlab.page.actions.disconnect': '연결 해제', + 'settings.gitlab.page.actions.switch': '전환', + 'settings.gitlab.page.status.notConnected': '연결되지 않음', + 'settings.gitlab.page.label.unknownUser': '알 수 없음', + 'settings.gitlab.page.label.otherAccounts': '기타 계정', + 'settings.gitlab.page.avatarAlt.withLogin': '{login} 아바타', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab 아바타', + 'settings.gitlab.page.connectedAs': '연결된 계정:', + 'settings.gitlab.page.errors.invalidToken': '유효한 GitLab 개인 액세스 토큰을 입력하세요', + 'settings.gitlab.page.errors.failed': 'GitLab에 연결하지 못했습니다', + 'settings.gitlab.page.toast.connected': 'GitLab에 연결되었습니다', + 'settings.gitlab.page.toast.disconnected': 'GitLab 연결이 해제되었습니다', + 'settings.gitlab.page.toast.disconnectFailed': 'GitLab 연결을 해제하지 못했습니다', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab 계정이 전환되었습니다', + 'settings.gitlab.page.toast.accountSwitchFailed': 'GitLab 계정을 전환하지 못했습니다', 'settings.notifications.page.delivery.title': '알림', 'settings.notifications.page.delivery.enableAria': '알림 활성화', 'settings.notifications.page.delivery.enableLabel': '알림 활성화', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 5fe473cb..cbc5fa6c 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -326,6 +326,29 @@ export const settingsDict = { 'settings.github.page.ghCli.actions.disable': 'Wyłącz', 'settings.github.page.ghCli.actions.enable': 'Włącz', 'settings.github.page.tooltip.connectAccount': 'Połącz konto GitHub, aby korzystać z przepływów pracy dla PR i Issue w aplikacji.', + 'settings.gitlab.page.title': 'Osobisty token dostępu GitLab', + 'settings.gitlab.page.description': 'Wklej osobisty token dostępu GitLab, aby się połączyć. Ustaw podstawowy adres URL, gdy używasz własnej instancji GitLab.', + 'settings.gitlab.page.tooltip.connectAccount': 'Połącz konto GitLab, aby korzystać z przepływów pracy dla Issues i Merge Requestów w aplikacji.', + 'settings.gitlab.page.accessToken.label': 'Osobisty token dostępu', + 'settings.gitlab.page.accessToken.placeholder': 'Wklej swój osobisty token dostępu GitLab', + 'settings.gitlab.page.baseUrl.label': 'Podstawowy URL (opcjonalnie)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': 'Połącz GitLab', + 'settings.gitlab.page.actions.disconnect': 'Odłącz', + 'settings.gitlab.page.actions.switch': 'Przełącz na', + 'settings.gitlab.page.status.notConnected': 'Brak połączenia', + 'settings.gitlab.page.label.unknownUser': 'nieznany', + 'settings.gitlab.page.label.otherAccounts': 'Inne konta', + 'settings.gitlab.page.avatarAlt.withLogin': 'Awatar {login}', + 'settings.gitlab.page.avatarAlt.fallback': 'Awatar GitLab', + 'settings.gitlab.page.connectedAs': 'Połączono jako', + 'settings.gitlab.page.errors.invalidToken': 'Wprowadź prawidłowy osobisty token dostępu GitLab', + 'settings.gitlab.page.errors.failed': 'Nie udało się połączyć z GitLab', + 'settings.gitlab.page.toast.connected': 'Połączono z GitLab', + 'settings.gitlab.page.toast.disconnected': 'Odłączono od GitLab', + 'settings.gitlab.page.toast.disconnectFailed': 'Nie udało się odłączyć GitLab', + 'settings.gitlab.page.toast.accountSwitched': 'Konto GitLab zostało przełączone', + 'settings.gitlab.page.toast.accountSwitchFailed': 'Nie udało się przełączyć konta GitLab', 'settings.magicPrompts.page.actions.resetAllOverrides': 'Zresetuj wszystkie nadpisania', 'settings.magicPrompts.page.actions.resetToDefault': 'Zresetuj do domyślnych', 'settings.magicPrompts.page.actions.resetting': 'Resetowanie...', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 6a6efa84..471268b2 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1672,6 +1672,29 @@ export const settingsDict = { "settings.github.page.toast.ghCliEnabled": "Alternativa gh CLI ativada", "settings.github.page.toast.ghCliDisabled": "Alternativa gh CLI desativada", "settings.github.page.toast.ghCliUpdateFailed": "Falha ao atualizar configuração do gh CLI", + "settings.gitlab.page.title": "Token de acesso pessoal do GitLab", + "settings.gitlab.page.description": "Cole um token de acesso pessoal do GitLab para conectar. Defina a URL base ao usar uma instância GitLab auto-hospedada.", + "settings.gitlab.page.tooltip.connectAccount": "Conecte uma conta do GitLab para fluxos de trabalho de issues e merge requests no aplicativo.", + "settings.gitlab.page.accessToken.label": "Token de acesso pessoal", + "settings.gitlab.page.accessToken.placeholder": "Cole seu token de acesso pessoal do GitLab", + "settings.gitlab.page.baseUrl.label": "URL base (opcional)", + "settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com", + "settings.gitlab.page.actions.connect": "Conectar GitLab", + "settings.gitlab.page.actions.disconnect": "Desconectar", + "settings.gitlab.page.actions.switch": "Alternar para", + "settings.gitlab.page.status.notConnected": "Não conectado", + "settings.gitlab.page.label.unknownUser": "desconhecido", + "settings.gitlab.page.label.otherAccounts": "Outras contas", + "settings.gitlab.page.avatarAlt.withLogin": "Avatar de {login}", + "settings.gitlab.page.avatarAlt.fallback": "Avatar do GitLab", + "settings.gitlab.page.connectedAs": "Conectado como", + "settings.gitlab.page.errors.invalidToken": "Insira um token de acesso pessoal do GitLab válido", + "settings.gitlab.page.errors.failed": "Falha ao conectar o GitLab", + "settings.gitlab.page.toast.connected": "GitLab conectado", + "settings.gitlab.page.toast.disconnected": "GitLab desconectado", + "settings.gitlab.page.toast.disconnectFailed": "Falha ao desconectar o GitLab", + "settings.gitlab.page.toast.accountSwitched": "Conta do GitLab alterada", + "settings.gitlab.page.toast.accountSwitchFailed": "Falha ao alternar a conta do GitLab", "settings.notifications.page.delivery.title": "Entrega de notificações", "settings.notifications.page.delivery.enableAria": "Ativar notificações", "settings.notifications.page.delivery.enableLabel": "Ativar notificações", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 3b54c685..b5f09da6 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1672,6 +1672,29 @@ export const settingsDict = { "settings.github.page.toast.ghCliEnabled": "Резервний варіант gh CLI увімкнено", "settings.github.page.toast.ghCliDisabled": "Резервний варіант gh CLI вимкнено", "settings.github.page.toast.ghCliUpdateFailed": "Не вдалося оновити налаштування gh CLI", + "settings.gitlab.page.title": "Персональний токен доступу GitLab", + "settings.gitlab.page.description": "Вставте персональний токен доступу GitLab, щоб підключитися. Вкажіть базову URL-адресу, якщо використовуєте самостійно розміщену інстанцію GitLab.", + "settings.gitlab.page.tooltip.connectAccount": "Підключіть обліковий запис GitLab для роботи з issues та merge requests в застосунку.", + "settings.gitlab.page.accessToken.label": "Персональний токен доступу", + "settings.gitlab.page.accessToken.placeholder": "Вставте ваш персональний токен доступу GitLab", + "settings.gitlab.page.baseUrl.label": "Базова URL-адреса (необов'язково)", + "settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com", + "settings.gitlab.page.actions.connect": "Підключити GitLab", + "settings.gitlab.page.actions.disconnect": "Відключити", + "settings.gitlab.page.actions.switch": "Перемкнути на", + "settings.gitlab.page.status.notConnected": "Не підключено", + "settings.gitlab.page.label.unknownUser": "невідомо", + "settings.gitlab.page.label.otherAccounts": "Інші облікові записи", + "settings.gitlab.page.avatarAlt.withLogin": "Аватар {login}", + "settings.gitlab.page.avatarAlt.fallback": "Аватар GitLab", + "settings.gitlab.page.connectedAs": "Підключено як", + "settings.gitlab.page.errors.invalidToken": "Введіть дійсний персональний токен доступу GitLab", + "settings.gitlab.page.errors.failed": "Не вдалося підключити GitLab", + "settings.gitlab.page.toast.connected": "GitLab підключено", + "settings.gitlab.page.toast.disconnected": "GitLab відключено", + "settings.gitlab.page.toast.disconnectFailed": "Не вдалося відключити GitLab", + "settings.gitlab.page.toast.accountSwitched": "Обліковий запис GitLab перемкнено", + "settings.gitlab.page.toast.accountSwitchFailed": "Не вдалося перемкнути обліковий запис GitLab", "settings.notifications.page.delivery.title": "Доставка сповіщень", "settings.notifications.page.delivery.enableAria": "Увімкнути сповіщення", "settings.notifications.page.delivery.enableLabel": "Увімкнути сповіщення", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index a2a459b0..e362fe0d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1672,6 +1672,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI 备用已启用', 'settings.github.page.toast.ghCliDisabled': 'gh CLI 备用已禁用', 'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 设置失败', + 'settings.gitlab.page.title': 'GitLab 个人访问令牌', + 'settings.gitlab.page.description': '粘贴 GitLab 个人访问令牌以连接。使用自托管 GitLab 实例时,请设置基础 URL。', + 'settings.gitlab.page.tooltip.connectAccount': '连接 GitLab 账户,以便在应用内使用 Issue 和合并请求工作流。', + 'settings.gitlab.page.accessToken.label': '个人访问令牌', + 'settings.gitlab.page.accessToken.placeholder': '粘贴你的 GitLab 个人访问令牌', + 'settings.gitlab.page.baseUrl.label': '基础 URL(可选)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': '连接 GitLab', + 'settings.gitlab.page.actions.disconnect': '断开连接', + 'settings.gitlab.page.actions.switch': '切换到', + 'settings.gitlab.page.status.notConnected': '未连接', + 'settings.gitlab.page.label.unknownUser': '未知', + 'settings.gitlab.page.label.otherAccounts': '其他账户', + 'settings.gitlab.page.avatarAlt.withLogin': '{login} 的头像', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab 头像', + 'settings.gitlab.page.connectedAs': '已连接为', + 'settings.gitlab.page.errors.invalidToken': '请输入有效的 GitLab 个人访问令牌', + 'settings.gitlab.page.errors.failed': '连接 GitLab 失败', + 'settings.gitlab.page.toast.connected': 'GitLab 已连接', + 'settings.gitlab.page.toast.disconnected': 'GitLab 已断开连接', + 'settings.gitlab.page.toast.disconnectFailed': '断开 GitLab 失败', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab 账户已切换', + 'settings.gitlab.page.toast.accountSwitchFailed': '切换 GitLab 账户失败', 'settings.notifications.page.delivery.title': '通知投递', 'settings.notifications.page.delivery.enableAria': '启用通知', 'settings.notifications.page.delivery.enableLabel': '启用通知', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index b25bf8ce..16000f19 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1579,6 +1579,29 @@ export const settingsDict = { 'settings.github.page.toast.ghCliEnabled': 'gh CLI 備用已啟用', 'settings.github.page.toast.ghCliDisabled': 'gh CLI 備用已停用', 'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 設定失敗', + 'settings.gitlab.page.title': 'GitLab 個人存取權杖', + 'settings.gitlab.page.description': '貼上 GitLab 個人存取權杖以連線。使用自架 GitLab 執行個體時,請設定基礎 URL。', + 'settings.gitlab.page.tooltip.connectAccount': '連線 GitLab 帳號,以在應用程式內使用 Issue 與合併請求工作流程。', + 'settings.gitlab.page.accessToken.label': '個人存取權杖', + 'settings.gitlab.page.accessToken.placeholder': '貼上你的 GitLab 個人存取權杖', + 'settings.gitlab.page.baseUrl.label': '基礎 URL(選用)', + 'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com', + 'settings.gitlab.page.actions.connect': '連線 GitLab', + 'settings.gitlab.page.actions.disconnect': '中斷連線', + 'settings.gitlab.page.actions.switch': '切換到', + 'settings.gitlab.page.status.notConnected': '未連線', + 'settings.gitlab.page.label.unknownUser': '未知', + 'settings.gitlab.page.label.otherAccounts': '其他帳號', + 'settings.gitlab.page.avatarAlt.withLogin': '{login} 頭像', + 'settings.gitlab.page.avatarAlt.fallback': 'GitLab 頭像', + 'settings.gitlab.page.connectedAs': '已連線為', + 'settings.gitlab.page.errors.invalidToken': '請輸入有效的 GitLab 個人存取權杖', + 'settings.gitlab.page.errors.failed': '連線 GitLab 失敗', + 'settings.gitlab.page.toast.connected': 'GitLab 已連線', + 'settings.gitlab.page.toast.disconnected': 'GitLab 已中斷連線', + 'settings.gitlab.page.toast.disconnectFailed': '中斷 GitLab 失敗', + 'settings.gitlab.page.toast.accountSwitched': 'GitLab 帳號已切換', + 'settings.gitlab.page.toast.accountSwitchFailed': '切換 GitLab 帳號失敗', 'settings.notifications.page.delivery.title': '通知傳遞', 'settings.notifications.page.delivery.enableAria': '啟用通知', 'settings.notifications.page.delivery.enableLabel': '啟用通知', diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index 2dba4ed1..3b7a4b2c 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -495,6 +495,12 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ titleKey: 'settings.github.page.actions.connect', keywords: ['github', 'account', 'oauth', 'prs', 'issues'], }, + { + id: 'git.gitlab-account', + page: 'git', + titleKey: 'settings.gitlab.page.actions.connect', + keywords: ['gitlab', 'account', 'pat', 'personal access token', 'issues', 'merge requests'], + }, { id: 'git.identities', page: 'git', diff --git a/packages/ui/src/stores/useGitLabAuthStore.ts b/packages/ui/src/stores/useGitLabAuthStore.ts new file mode 100644 index 00000000..ec96a5d9 --- /dev/null +++ b/packages/ui/src/stores/useGitLabAuthStore.ts @@ -0,0 +1,72 @@ +import { create } from 'zustand'; +import type { GitLabAuthStatus, RuntimeAPIs } from '@/lib/api/types'; +import { runtimeFetch } from '@/lib/runtime-fetch'; + +type GitLabAuthStatusWithError = GitLabAuthStatus & { error?: string }; + +type GitLabAuthStore = { + status: GitLabAuthStatusWithError | null; + isLoading: boolean; + hasChecked: boolean; + setStatus: (status: GitLabAuthStatusWithError | null) => void; + refreshStatus: ( + runtimeGitLab?: RuntimeAPIs['gitlab'], + options?: { force?: boolean } + ) => Promise; +}; + +const fetchStatus = async ( + runtimeGitLab?: RuntimeAPIs['gitlab'] +): Promise => { + if (runtimeGitLab) { + const payload = await runtimeGitLab.authStatus(); + return payload as GitLabAuthStatus; + } + + const response = await runtimeFetch('/api/gitlab/auth/status', { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + const payload = (await response.json().catch(() => null)) as GitLabAuthStatusWithError | null; + if (!response.ok || !payload) { + throw new Error(payload?.error || response.statusText || 'Failed to load GitLab status'); + } + return payload; +}; + +// In-flight dedup for refreshStatus +let _inFlightAuthRefresh: Promise | null = null; + +export const useGitLabAuthStore = create((set, get) => ({ + status: null, + isLoading: false, + hasChecked: false, + setStatus: (status) => set({ status, hasChecked: true }), + refreshStatus: async (runtimeGitLab, options) => { + const { hasChecked, status } = get(); + if (hasChecked && !options?.force) { + return status; + } + + if (_inFlightAuthRefresh) return _inFlightAuthRefresh; + + set({ isLoading: true }); + _inFlightAuthRefresh = (async () => { + try { + const payload = await fetchStatus(runtimeGitLab); + set({ status: payload, isLoading: false, hasChecked: true }); + return payload; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + set({ + status: { connected: false, accounts: [], defaultBaseUrl: '', error: message }, + isLoading: false, + hasChecked: true, + }); + return null; + } + })().finally(() => { _inFlightAuthRefresh = null; }); + + return _inFlightAuthRefresh; + }, +}));