From d47bd23106e56e923aa99f3a514e13ed0e7df96d Mon Sep 17 00:00:00 2001 From: Daniel Krizan Date: Mon, 16 Mar 2026 15:53:33 +0100 Subject: [PATCH 1/6] feat: add branching support to chrome extension Allow users to select a branch when configuring the Tolgee chrome plugin. The branch field is only shown when branching is enabled on the project, determined by the branchingEnabled flag from the API key validation response. --- src/constants.ts | 1 + src/content/contentScript.ts | 12 +++++++++++- src/popup/TolgeeDetector.tsx | 20 +++++++++++++++++++- src/popup/storage.ts | 3 +++ src/popup/tools.ts | 1 + src/popup/useDetectorForm.tsx | 5 +++++ src/types.ts | 1 + 7 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2573e23..f630aac 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,3 @@ export const API_KEY_LOCAL_STORAGE = '__tolgee_apiKey'; export const API_URL_LOCAL_STORAGE = '__tolgee_apiUrl'; +export const BRANCH_LOCAL_STORAGE = '__tolgee_branch'; diff --git a/src/content/contentScript.ts b/src/content/contentScript.ts index 9b7c6ee..d9f560a 100644 --- a/src/content/contentScript.ts +++ b/src/content/contentScript.ts @@ -1,4 +1,8 @@ -import { API_KEY_LOCAL_STORAGE, API_URL_LOCAL_STORAGE } from '../constants'; +import { + API_KEY_LOCAL_STORAGE, + API_URL_LOCAL_STORAGE, + BRANCH_LOCAL_STORAGE, +} from '../constants'; import { LibConfig } from '../types'; import { injectUiLib } from './injectUiLib'; import { Messages } from './Messages'; @@ -13,6 +17,7 @@ const getAppliedCredenials = () => { return { apiKey: sessionStorage.getItem(API_KEY_LOCAL_STORAGE), apiUrl: sessionStorage.getItem(API_URL_LOCAL_STORAGE), + branch: sessionStorage.getItem(BRANCH_LOCAL_STORAGE), }; }; @@ -72,6 +77,11 @@ messages.listenRuntime('SET_CREDENTIALS', async (data) => { } else { sessionStorage.removeItem(API_URL_LOCAL_STORAGE); } + if (data.branch) { + sessionStorage.setItem(BRANCH_LOCAL_STORAGE, data.branch); + } else { + sessionStorage.removeItem(BRANCH_LOCAL_STORAGE); + } location.reload(); updateState(configuration, messages); }); diff --git a/src/popup/TolgeeDetector.tsx b/src/popup/TolgeeDetector.tsx index 03deb90..db569ac 100644 --- a/src/popup/TolgeeDetector.tsx +++ b/src/popup/TolgeeDetector.tsx @@ -66,7 +66,8 @@ export const TolgeeDetector = () => { const valuesNotChanged = isInDevelopmentMode && libConfig?.config.apiKey === values?.apiKey && - libConfig?.config.apiUrl === values?.apiUrl; + libConfig?.config.apiUrl === values?.apiUrl && + (libConfig?.config.branch || '') === (values?.branch || ''); return ( { )} + {typeof credentialsCheck === 'object' && + credentialsCheck?.branchingEnabled && ( + + dispatch({ + type: 'CHANGE_VALUES', + payload: { branch: e.target.value }, + }) + } + onKeyDown={handleKeyDown} + size="small" + placeholder={libConfig?.config?.branch || 'Default branch'} + /> + )} { @@ -25,6 +26,7 @@ export const storeValues = async (values: Values | null) => { [origin]: { apiUrl: values.apiUrl, apiKey: values.apiKey, + branch: values.branch, }, }); } else { @@ -45,6 +47,7 @@ export const loadValues = async () => { return { apiKey: data?.apiKey, apiUrl: data?.apiUrl, + branch: data?.branch, }; } catch (e) { console.error(e); diff --git a/src/popup/tools.ts b/src/popup/tools.ts index 6d97a7e..bdc8526 100644 --- a/src/popup/tools.ts +++ b/src/popup/tools.ts @@ -1,6 +1,7 @@ export type Values = { apiUrl?: string; apiKey?: string; + branch?: string; }; export const validateValues = (values?: Values | null) => { diff --git a/src/popup/useDetectorForm.tsx b/src/popup/useDetectorForm.tsx index 7da82c6..918634c 100644 --- a/src/popup/useDetectorForm.tsx +++ b/src/popup/useDetectorForm.tsx @@ -13,6 +13,7 @@ type ProjectInfo = { projectName: string; scopes: string[]; userFullName: string; + branchingEnabled: boolean; }; type CredentialsCheck = null | 'loading' | 'invalid' | ProjectInfo; @@ -57,6 +58,7 @@ export const useDetectorForm = () => { const newValues = { apiKey: libData?.config?.apiKey, apiUrl: libData?.config?.apiUrl, + branch: libData?.config?.branch, }; if (state.libConfig !== null && state.frameId !== frameId) { return { @@ -106,10 +108,12 @@ export const useDetectorForm = () => { appliedValues: { apiKey: state.values?.apiKey, apiUrl: state.values?.apiUrl, + branch: state.values?.branch, }, storedValues: { apiKey: state.values?.apiKey, apiUrl: state.values?.apiUrl, + branch: state.values?.branch, }, }; case 'CLEAR_ALL': { @@ -262,6 +266,7 @@ export const useDetectorForm = () => { projectName: data.projectName, scopes: data.scopes, userFullName: data.userFullName, + branchingEnabled: data.branchingEnabled ?? false, }); }); } else { diff --git a/src/types.ts b/src/types.ts index b58f096..8cbfac0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export type LibConfig = { config: { apiUrl: ''; apiKey: ''; + branch?: string; // @deprecated older versions mode?: 'production' | 'development'; }; From 94f338cd931e60d086da12b04ccb5e3df92a7871 Mon Sep 17 00:00:00 2001 From: Daniel Krizan Date: Mon, 16 Mar 2026 19:02:37 +0100 Subject: [PATCH 2/6] fix: compareValues tautology and stale branch when branching disabled --- src/popup/tools.ts | 4 +++- src/popup/useDetectorForm.tsx | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/popup/tools.ts b/src/popup/tools.ts index bdc8526..5f331e0 100644 --- a/src/popup/tools.ts +++ b/src/popup/tools.ts @@ -16,7 +16,9 @@ export const compareValues = ( values2?: Values | null ) => { return ( - values1?.apiKey === values2?.apiKey && values2?.apiUrl === values2?.apiUrl + values1?.apiKey === values2?.apiKey && + values1?.apiUrl === values2?.apiUrl && + (values1?.branch || '') === (values2?.branch || '') ); }; diff --git a/src/popup/useDetectorForm.tsx b/src/popup/useDetectorForm.tsx index 918634c..07f9012 100644 --- a/src/popup/useDetectorForm.tsx +++ b/src/popup/useDetectorForm.tsx @@ -100,22 +100,29 @@ export const useDetectorForm = () => { storedValues: action.payload, values: action.payload, }; - case 'APPLY_VALUES': + case 'APPLY_VALUES': { // sync values with storage/localStorage apply(); + const branchEnabled = + typeof state.credentialsCheck === 'object' && + state.credentialsCheck?.branchingEnabled; + const effectiveBranch = branchEnabled + ? state.values?.branch + : undefined; return { ...state, appliedValues: { apiKey: state.values?.apiKey, apiUrl: state.values?.apiUrl, - branch: state.values?.branch, + branch: effectiveBranch, }, storedValues: { apiKey: state.values?.apiKey, apiUrl: state.values?.apiUrl, - branch: state.values?.branch, + branch: effectiveBranch, }, }; + } case 'CLEAR_ALL': { apply(); return { From b6580fe936e70ebfa5b19caac74a24bdd0bfd072 Mon Sep 17 00:00:00 2001 From: Daniel Krizan Date: Mon, 16 Mar 2026 19:09:06 +0100 Subject: [PATCH 3/6] chore: add helper text to branch field explaining SDK inheritance --- src/popup/TolgeeDetector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/popup/TolgeeDetector.tsx b/src/popup/TolgeeDetector.tsx index db569ac..1e97a5c 100644 --- a/src/popup/TolgeeDetector.tsx +++ b/src/popup/TolgeeDetector.tsx @@ -142,6 +142,7 @@ export const TolgeeDetector = () => { onKeyDown={handleKeyDown} size="small" placeholder={libConfig?.config?.branch || 'Default branch'} + helperText="Leave empty to use the branch from SDK config" /> )} Date: Mon, 16 Mar 2026 19:22:40 +0100 Subject: [PATCH 4/6] chore: update actions/cache to v4 in test workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0923c4..05449b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: node-version: '20.x' - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.npm key: cache-node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} From 9639f3161390a349e8bbc223d71f320e7f53f6f5 Mon Sep 17 00:00:00 2001 From: Daniel Krizan Date: Wed, 18 Mar 2026 15:09:15 +0100 Subject: [PATCH 5/6] feat: replace branch text field with search select dropdown Fetch available branches from the platform API after credential validation and display them in an Autocomplete dropdown. Users can select from existing branches or type a custom name. The dropdown only appears when branching is enabled on the project. --- src/popup/TolgeeDetector.tsx | 78 +++++++++++++++++++++++++++++------ src/popup/useDetectorForm.tsx | 56 ++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/src/popup/TolgeeDetector.tsx b/src/popup/TolgeeDetector.tsx index 1e97a5c..310d005 100644 --- a/src/popup/TolgeeDetector.tsx +++ b/src/popup/TolgeeDetector.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { + Autocomplete, Box, Button, CircularProgress, @@ -26,7 +27,9 @@ export const TolgeeDetector = () => { libConfig, tolgeePresent, credentialsCheck, + branches, } = state; + const [branchOpen, setBranchOpen] = useState(false); const handleApplyChange = async () => { if (appliedValues) { @@ -129,20 +132,69 @@ export const TolgeeDetector = () => { {typeof credentialsCheck === 'object' && credentialsCheck?.branchingEnabled && ( - + setBranchOpen(true)} + onClose={() => setBranchOpen(false)} + freeSolo + size="small" + disablePortal + slotProps={{ + popper: { + placement: 'bottom', + modifiers: [{ name: 'flip', enabled: false }], + }, + }} + ListboxProps={{ style: { maxHeight: 150 } }} + options={branches ?? []} + getOptionLabel={(option) => + typeof option === 'string' ? option : option.name + } + value={ + branches?.find((b) => b.name === values?.branch) ?? + values?.branch ?? + null + } + onChange={(_e: any, newValue: any) => { dispatch({ type: 'CHANGE_VALUES', - payload: { branch: e.target.value }, - }) - } - onKeyDown={handleKeyDown} - size="small" - placeholder={libConfig?.config?.branch || 'Default branch'} - helperText="Leave empty to use the branch from SDK config" + payload: { + branch: + typeof newValue === 'string' + ? newValue + : newValue?.name ?? '', + }, + }); + }} + onInputChange={(_e: any, newInput: string, reason: string) => { + if (reason === 'input') { + dispatch({ + type: 'CHANGE_VALUES', + payload: { branch: newInput }, + }); + } + }} + renderOption={(props, option) => ( +
  • + {option.name} + {option.isDefault && ( + + default + + )} +
  • + )} + renderInput={(params) => ( + + )} /> )} { const { applyRequired, apply } = useApplier(); @@ -148,6 +156,11 @@ export const useDetectorForm = () => { appliedValues: state.storedValues, values: state.storedValues, }; + case 'SET_BRANCHES': + return { + ...state, + branches: action.payload, + }; default: // @ts-expect-error action type is type uknown throw new Error(`Unknown action ${action.type}`); @@ -271,6 +284,7 @@ export const useDetectorForm = () => { data && setCredentialsCheck({ projectName: data.projectName, + projectId: data.projectId, scopes: data.scopes, userFullName: data.userFullName, branchingEnabled: data.branchingEnabled ?? false, @@ -284,5 +298,45 @@ export const useDetectorForm = () => { }; }, [checkableValues?.apiUrl, checkableValues?.apiKey]); + // fetch branches when credentials are valid and branching is enabled + useEffect(() => { + let cancelled = false; + const check = state.credentialsCheck; + if ( + typeof check === 'object' && + check?.branchingEnabled && + validateValues(checkableValues) + ) { + const url = normalizeUrl(checkableValues!.apiUrl); + fetch( + `${url}/v2/projects/${check.projectId}/branches?ak=${ + checkableValues!.apiKey + }&size=100` + ) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (!cancelled && data?._embedded?.branches) { + dispatch({ + type: 'SET_BRANCHES', + payload: data._embedded.branches.map((b: any) => ({ + name: b.name, + isDefault: b.isDefault, + })), + }); + } + }) + .catch(() => { + if (!cancelled) { + dispatch({ type: 'SET_BRANCHES', payload: null }); + } + }); + } else { + dispatch({ type: 'SET_BRANCHES', payload: null }); + } + return () => { + cancelled = true; + }; + }, [state.credentialsCheck]); + return [state, dispatch] as const; }; From 7fb0d37f4ed60c18f172df8d45026df9b98eec4e Mon Sep 17 00:00:00 2001 From: Daniel Krizan Date: Wed, 18 Mar 2026 15:56:24 +0100 Subject: [PATCH 6/6] fix: prettier formatting and clear stale branches on failed response Fix prettier multiline formatting issue. Also fix a bug where non-2xx branch fetch responses returned null instead of throwing, leaving stale branches from a previous project visible in the dropdown. --- src/popup/useDetectorForm.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/popup/useDetectorForm.tsx b/src/popup/useDetectorForm.tsx index 1e177a4..735f5a6 100644 --- a/src/popup/useDetectorForm.tsx +++ b/src/popup/useDetectorForm.tsx @@ -313,15 +313,21 @@ export const useDetectorForm = () => { checkableValues!.apiKey }&size=100` ) - .then((r) => (r.ok ? r.json() : null)) + .then((r) => { + if (!r.ok) { + throw new Error('Failed to load branches'); + } + return r.json(); + }) .then((data) => { - if (!cancelled && data?._embedded?.branches) { + if (!cancelled) { dispatch({ type: 'SET_BRANCHES', - payload: data._embedded.branches.map((b: any) => ({ - name: b.name, - isDefault: b.isDefault, - })), + payload: + data?._embedded?.branches?.map((b: any) => ({ + name: b.name, + isDefault: b.isDefault, + })) ?? null, }); } })