Skip to content

Feat implement branching#94

Open
eweren wants to merge 13 commits intomainfrom
feat-implement-branching
Open

Feat implement branching#94
eweren wants to merge 13 commits intomainfrom
feat-implement-branching

Conversation

@eweren
Copy link
Copy Markdown
Collaborator

@eweren eweren commented Mar 19, 2026

Summary by CodeRabbit

  • New Features

    • Project branching UI with branch selector; branch-aware pull/push flows and branch parameter support; branch list refresh.
  • Performance Improvements

    • Query caching updated (5 min cache / 30s freshness); memoized components/hooks and consolidated parent lookup for faster renders.
  • Bug Fixes

    • Improved frame grouping for screenshot collection and more reliable translation diffing.
  • Chores

    • ESLint/TypeScript/Prettier/tooling upgrades, assorted UI/formatting refinements, and select overflow/ellipsis styling.

eweren and others added 10 commits March 6, 2026 10:04
…n keys

- Fix memory leak in useSelectedNodes (missing dep array, debounce dual events)
- Stabilize ListItem handlers with useCallback, consolidate race-prone effects
- Memoize props in Index, NamespaceSelect, ProjectSettings, AutocompleteSelect
- Wrap KeyInput and NodeRow with memo to prevent unnecessary re-renders
- Set reasonable React Query cache/stale defaults (was 0/0)
- Reduce Connect search debounce from 1000ms to 300ms
- Cache figma.listAvailableFontsAsync() per formatText call
- Optimize getConflictingNodes from O(n²) to O(n) using Map
- Optimize getPushChanges screenshot lookup from O(n×m×k) to O(n)
- Eliminate redundant node traversal in getScreenshots
- Combine 6 parent traversals into single getAllParents() in preformatKey
- Fix stale closures in Dialog, Popover, and ResizeHandle
- Extract inline styles in NodeList virtualization
- Disable virtualization in Push Changes view to fix blank space issue
- Precompute toLowerCase/highlight data in AutocompleteSelect
- Remove layout thrashing from AutocompleteSelect useLayoutEffect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix hooks rule violation in NodeList (useMemo after early return)
- Preserve empty namespace option in NamespaceSelect (remove .filter(Boolean))
- Add staleTime/cacheTime: 0 to useSelectedNodes for fresh data on remount
- Use consistent trimmed input for filtering and matching in AutocompleteSelect
- Use displayValue labels in highlight rendering path in AutocompleteSelect
- Handle empty namespace array fallback in ProjectSettings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Introduced normalized comparison for already selected values in multi-select mode.
- Updated filtering logic to utilize a Set for performance optimization.
- Ensured consistent casing and trimming for input values during comparisons.
- Updated package.json to use the correct API endpoint for schema generation.
- Introduced optional 'branch' property in CurrentDocumentSettings type.
- Enhanced setPluginData function to handle branch data.
- Added BranchModel interface and new API endpoints for branch management in apiSchema.
- Updated various components and hooks to support branch selection and data fetching.
- Integrated BranchSelect component in ProjectSettings for user branch selection.
- Implemented logic to handle default branch settings and display in the UI.
@eweren eweren requested a review from JanCizmar March 19, 2026 14:05
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

Walkthrough

Adds an ESLint v9 flat config and tooling upgrades; implements branching support across API schema, client types, hooks, and UI; refactors parent-node traversal and several tools for efficiency; introduces font caching for text formatting; applies memoization, debouncing, and query caching improvements; and applies widespread trailing-comma/formatting consistency.

Changes

Cohort / File(s) Summary
ESLint & Tooling
eslint.config.js, package.json, tsconfig.json
Add ESLint v9 flat config, upgrade ESLint/TS/Prettier/tooling and related deps; update npm scripts; add compilerOptions.types.
Branching API & Types
src/types.ts, src/ui/client/apiSchema.custom.ts, src/ui/client/apiSchema.generated.ts
Add branch to document settings/types, introduce BranchModel, add branches/project-info endpoints, and propagate branch query params and useBranching schema/property.
Parent Traversal Refactor
src/main/utils/nodeParents.ts, src/main/endpoints/preformatKey.ts
Add getAllParents(nodeId) and NodeParents type; refactor existing parent getters and preformatKey to use aggregated parent lookup.
Font Caching & Text Formatting
src/main/endpoints/formatText.ts, src/createFormatIcu.ts
Add cached getAvailableFonts() to avoid repeated font list calls; small ICU formatter formatting tweak.
Screenshots & Conflict Detection
src/main/endpoints/getScreenshots.ts, src/tools/getConflictingNodes.ts
Group text nodes by frame via Map for screenshots; replace pairwise duplicate detection with grouping by composite key for conflict detection.
Push/Pull & Namespace Handling
src/tools/getPullChanges.ts, src/tools/getPushChanges.ts, src/ui/views/Push/Push.tsx, src/ui/views/Pull/Pull.tsx
Make pull/push branch- and namespace-aware, add screenshotsByKey lookup, change push DTO/endpoint, set OVERRIDE resolution and include overrideMode/branch in push payloads.
Branch UI & Hooks
src/ui/components/BranchSelect/..., src/ui/hooks/useHasBranchingEnabled.ts, src/ui/views/Settings/ProjectSettings.tsx, src/ui/views/Index/Index.tsx, src/ui/views/Pull/Pull.tsx
Add BranchSelect component and styles, new useHasBranchingEnabled hook, fetch branches, and wire branching into settings, index, and pull views.
Memoization, Debouncing & UX
src/ui/components/AutocompleteSelect/..., src/ui/components/NodeList/NodeRow.tsx, src/ui/components/Popover/Popover.tsx, src/ui/components/ResizeHandle/ResizeHandle.tsx, src/ui/views/Index/ListItem.tsx, src/ui/views/Index/KeyInput.tsx, src/ui/hooks/useSelectedNodes.ts, src/ui/hooks/useConnectedNodes.ts
Add memoization (memo, useCallback, useMemo), normalize filtering, debounce refetches, consolidate node update mutations, and include ignoreSelection in query keys.
Client API & Generated Types
src/ui/client/client.ts, src/ui/client/types.ts, src/ui/client/useQueryApi.ts, src/ui/client/decodeApiKey.ts, src/ui/client/apiSchema.custom.ts
Formatting/type refinements, branch-aware params, and small parsing/ESLint adjustments in API client helpers.
UI Tuning & Query Behavior
src/ui/Plugin.tsx, src/ui/styles.css, src/ui/views/Connect/Connect.tsx, src/ui/views/CreateCopy/CreateCopy.tsx, src/ui/views/Push/Changes.tsx
Change QueryClient cache/stale times, limit select overflow with ellipsis, shorten search debounce, constrain list height, and set minVisibleRows={Infinity} for NodeLists.
Settings & Strings Editing
src/ui/views/Settings/StringsEditor.tsx, src/ui/views/Settings/ProjectSettings.tsx, src/ui/views/Settings/PushSection.tsx, src/ui/views/Settings/StringsSection.tsx
Integrate branch selector into project settings, fetch branches, memoize namespaces, and adjust strings editor / push settings UI behaviors.
Formatting / Trailing Commas
many src/... files (widespread)
Apply consistent trailing-comma and formatting changes across numerous files (mostly syntactic, lint/Prettier alignment).

Sequence Diagram(s)

sequenceDiagram
  participant UI as Plugin UI
  participant Hook as useHasBranchingEnabled
  participant APIKey as GET /v2/api-keys/current
  participant APIProj as GET /v2/projects/{projectId}/branches

  UI->>Hook: call useHasBranchingEnabled()
  Hook->>APIKey: GET /v2/api-keys/current
  APIKey-->>Hook: { projectId }
  Hook->>APIProj: GET /v2/projects/{projectId}
  APIProj-->>Hook: { ProjectModel (useBranching), branches }
  Hook-->>UI: return hasBranchingEnabled & branches
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • JanCizmar
  • stepan662

Poem

🐰 I hopped through code with commas in tow,

I cached the fonts and made lookups go,
Found branches to fetch and parents to trace,
Memoized paths for a faster race,
A tiny rabbit tweak — now onward we go!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat implement branching' directly describes the main feature addition throughout the changeset—introducing branch selection, branch-aware API queries, and branch configuration support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-implement-branching

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/ui/components/Dialog/Dialog.tsx (1)

13-22: ⚠️ Potential issue | 🟡 Minor

Capture ref.current before cleanup to avoid stale reference.

Adding onClose to the dependency array is correct and fixes the stale closure. However, the cleanup function reads ref.current at cleanup time, which may differ from when the listener was added (especially now that the effect re-runs when onClose changes). Capture the element in a local variable:

Proposed fix
   useEffect(() => {
+    const element = ref.current;
     const handler = (e: KeyboardEvent) => {
       if (e.code === "Escape") {
         onClose();
       }
     };
-    ref.current?.focus();
-    ref.current?.addEventListener("keydown", handler);
-    return () => ref.current?.removeEventListener("keydown", handler);
+    element?.focus();
+    element?.addEventListener("keydown", handler);
+    return () => element?.removeEventListener("keydown", handler);
   }, [onClose]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/Dialog/Dialog.tsx` around lines 13 - 22, The effect
currently reads ref.current both when adding the keydown listener and again
during cleanup, which can lead to a stale/different element being used; capture
the element into a local constant (e.g., const el = ref.current) at the start of
the useEffect, call el?.focus() and el?.addEventListener("keydown", handler),
and use the same el in the cleanup to removeEventListener; keep the dependency
on onClose and reference handler within the effect so the correct listener is
removed.
src/ui/components/Popover/Popover.tsx (1)

63-75: ⚠️ Potential issue | 🟡 Minor

Capture popoverTrigger.current before cleanup.

Same issue as in Dialog.tsx: the cleanup function reads popoverTrigger.current at teardown time, which may be null or a different element if the ref changed. Capture it in a local variable:

Proposed fix
   useEffect(() => {
-    if (!popoverTrigger.current || !dropdownRef.current) {
+    const triggerElement = popoverTrigger.current;
+    if (!triggerElement || !dropdownRef.current) {
       return;
     }
 
-    popoverTrigger.current.addEventListener("click", open);
+    triggerElement.addEventListener("click", open);
     window.addEventListener("resize", computePositionAndSet);
 
     return () => {
-      popoverTrigger.current?.removeEventListener("click", open);
+      triggerElement.removeEventListener("click", open);
       window.removeEventListener("resize", computePositionAndSet);
     };
   }, [popoverTrigger, open, computePositionAndSet]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/Popover/Popover.tsx` around lines 63 - 75, The cleanup
currently reads popoverTrigger.current at teardown which can be null or changed;
inside the useEffect that registers listeners (useEffect referencing
popoverTrigger, open, computePositionAndSet) capture the current element into a
local const (e.g., const trigger = popoverTrigger.current) before adding
listeners and then remove listeners from that captured trigger in the return
cleanup; do the same for any other refs used (dropdownRef if you later use it)
so the removeEventListener calls target the same element instance that
addEventListener was called on.
src/ui/hooks/useInterpolatedTranslation.ts (1)

149-158: ⚠️ Potential issue | 🟠 Major

Preserve Tolgee markup when seeding interpolatedTranslation.current.

Line 153 strips tags before the equality check, so a translation like <b>Hello</b> now collapses to Hello and Line 155 skips the ref update when node.characters is already Hello. That leaves interpolatedTranslation.current as plain text, which can drop rich-text markup for later consumers.

💡 Suggested fix
  useEffect(() => {
-    const newCharacters = getOrUpdateInterpolatedTranslation({
+    const nextInterpolated = getOrUpdateInterpolatedTranslation({
       paramsValues: paramsValues || {},
       currentTranslation: newTranslation,
-    })?.replace(/<[^>]*>/g, "");
+    });
+    const newCharacters = nextInterpolated?.replace(/<[^>]*>/g, "");

-    if (newCharacters != null && newCharacters !== previousCharacters) {
-      interpolatedTranslation.current = newCharacters;
+    if (
+      nextInterpolated != null &&
+      (newCharacters !== previousCharacters ||
+        nextInterpolated !== previousCharacters)
+    ) {
+      interpolatedTranslation.current = nextInterpolated;
     }
   }, [rawTranslation, node?.isPlural]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/hooks/useInterpolatedTranslation.ts` around lines 149 - 158, The
effect currently strips HTML tags from the candidate translation before
comparing to previousCharacters, which causes the ref to drop Tolgee markup;
instead, call getOrUpdateInterpolatedTranslation once into a variable (e.g.,
newWithMarkup), derive a stripped version only for the equality check
(newStripped = newWithMarkup?.replace(/<[^>]*>/g, "")), and if newStripped is
non-null and differs from previousCharacters then assign
interpolatedTranslation.current = newWithMarkup so the markup is preserved;
update references to useEffect, getOrUpdateInterpolatedTranslation,
interpolatedTranslation.current, previousCharacters, rawTranslation and
node?.isPlural accordingly.
src/ui/views/StringDetails/StringDetails.tsx (1)

439-457: ⚠️ Potential issue | 🔴 Critical

Remove unsupported onChange prop; use onValueInput or onInput instead.

The TextboxNumeric component in @create-figma-plugin/ui v3.2.0 does not have an onChange event handler (removed as a breaking change in v3.0.1). Replace with one of:

  • onValueInput={(value: string) => { ... }} – receives the string input value
  • onNumericValueInput={(value: null | number) => { ... }} – receives parsed numeric value
  • onInput={(event: JSX.TargetedEvent) => { ... }} – raw event object if direct currentTarget.value access is needed

The current code will not execute because the prop doesn't exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/StringDetails/StringDetails.tsx` around lines 439 - 457,
TextboxNumeric's onChange prop is unsupported; replace it with onValueInput or
onInput and adapt the handler: move the logic that sets
setNeedsSubmission(true), updates currentNode.pluralParamValue and builds
newNode (NodeInfo) into an onValueInput={(value: string) => { ... }} (or onInput
to access event.currentTarget.value) and call setCurrentNode(newNode) only when
translation != null; ensure you keep the same checks for empty string and
preserve setting currentNode.pluralParamValue, setNeedsSubmission, and
construction of newNode so the behavior of TextboxNumeric, currentNode,
setCurrentNode, setNeedsSubmission and translation remains identical.
src/ui/views/CreateCopy/CreateCopy.tsx (1)

55-57: ⚠️ Potential issue | 🟠 Major

Pass the active branch parameter to translation fetches.

The allTranslationsLoadable.getData call is missing the branch parameter. When a non-default branch is selected, CreateCopy will fetch translations from the wrong dataset. Pull already passes branch correctly; apply the same pattern here.

Suggested fix
-import { useGlobalActions } from "@/ui/state/GlobalState";
+import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState";
...
 export const CreateCopy: FunctionComponent = () => {
   const { setRoute } = useGlobalActions();
+  const branch = useGlobalState((c) => c.config?.branch);
...
         const response = await allTranslationsLoadable.getData({
           language,
+          branch: branch || undefined,
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/CreateCopy/CreateCopy.tsx` around lines 55 - 57, The call to
allTranslationsLoadable.getData in CreateCopy.tsx is missing the branch
parameter so translations for non-default branches are not fetched; update the
call to pass the currently selected branch (e.g., the same branch variable/prop
used by the Pull component) by adding branch: activeBranch (or the existing
branch identifier in scope) to the getData argument object, ensuring CreateCopy
fetches translations from the correct branch.
🧹 Nitpick comments (13)
src/utilities.ts (1)

28-57: Apply trailing commas consistently across all switch cases.

Trailing commas were added to the camelCase (line 35) and PascalCase (line 43) branches, but the snake_case (line 50) and snake_case_capitalized (line 56) branches are missing them. For consistency, consider adding trailing commas to all similar map operations.

✨ Proposed fix for consistency
     case "snake_case":
       return str
         .split(/\s+/)
         .filter(Boolean)
-        .map((word) => word.toLowerCase())
+        .map((word) => word.toLowerCase(),)
         .join("_");
     case "snake_case_capitalized":
       return str
         .split(/\s+/)
         .filter(Boolean)
-        .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+        .map((word) => word.charAt(0).toUpperCase() + word.slice(1),)
         .join("_");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utilities.ts` around lines 28 - 57, The switch branches for "snake_case"
and "snake_case_capitalized" are missing trailing commas on their .map(...)
arrow function expressions, causing inconsistency with the "camelCase" and
"PascalCase" branches; update the .map callbacks inside the "snake_case" and
"snake_case_capitalized" cases to include trailing commas (same style as used in
the "camelCase" and "PascalCase" branches) so all map operations are
consistently formatted.
src/ui/styles.css (1)

49-50: Consider adding white-space: nowrap for reliable ellipsis behavior.

text-overflow: ellipsis typically requires white-space: nowrap to take effect. Without it, text may wrap instead of truncating with an ellipsis. Note that native <select> elements have limited and inconsistent cross-browser support for text-overflow styling.

♻️ Suggested improvement
 select {
   border: 1px solid var(--figma-color-border);
   overflow-x: hidden;
   text-overflow: ellipsis;
+  white-space: nowrap;
   border-radius: var(--border-radius-2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/styles.css` around lines 49 - 50, The current CSS rule that sets
"overflow-x: hidden" and "text-overflow: ellipsis" needs "white-space: nowrap"
added to ensure the ellipsis consistently appears; update the same selector (the
rule containing overflow-x and text-overflow in src/ui/styles.css) to include
white-space: nowrap, and if this targets native <select> elements note their
text-overflow support is inconsistent across browsers and consider using a
custom styled control if cross-browser truncation is required.
src/ui/views/StringDetails/StringDetails.tsx (1)

460-464: Use strict inequality for placeholder filtering.

On Line 463, prefer !== over != for clearer intent and to avoid coercion edge cases.

Proposed diff
-                      p.name != tolgeeValue.parameter,
+                      p.name !== tolgeeValue.parameter,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/StringDetails/StringDetails.tsx` around lines 460 - 464, The
filter predicate in the StringDetails component uses loose inequality when
comparing p.name and tolgeeValue.parameter; update the comparison in the lambda
passed to .filter (the function referencing tolgeeValue.parameter and p.name) to
use strict inequality !== instead of != to avoid coercion and make intent
explicit.
src/main/utils/nodeParents.ts (1)

52-80: Consider documenting the performance trade-off.

The refactored helpers now each invoke getAllParents, meaning multiple calls for the same node will traverse the parent chain multiple times. This is fine when callers use getAllParents directly (as in the updated preformatKey.ts), but could be a subtle performance regression for code that still calls multiple helpers separately.

Consider adding a brief JSDoc note suggesting callers use getAllParents directly when they need multiple parent types for the same node.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/nodeParents.ts` around lines 52 - 80, The helper accessors
(getFrame, getArtboard, getComponent, getSection, getGroup) each call
getAllParents individually which can re-traverse the parent chain and cause a
performance regression when multiple helpers are used for the same node; update
the JSDoc for these functions (and optionally the file header) to briefly note
this trade-off and recommend callers call getAllParents(nodeId) once and reuse
its returned .frame/.artboard/.component/.section/.group when they need multiple
parent types for the same node.
src/ui/components/NodeList/NodeRow.tsx (1)

21-30: Memoization added, but effectiveness depends on parent component.

Wrapping NodeRow with memo() is a reasonable optimization for virtualized lists. Note that memo() performs shallow prop comparison, so the memoization will only be effective if the parent component memoizes/stabilizes props like node, action, keyComponent, nsComponent, and onClick. If these are recreated on each render, the memo wrapper won't prevent re-renders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/NodeList/NodeRow.tsx` around lines 21 - 30, NodeRow is
wrapped with memo() but memoization will be ineffective if parent repeatedly
recreates props; ensure stable references for props like node, action,
keyComponent, nsComponent, onClick (or wrap them with useMemo/useCallback) in
the parent so shallow comparison succeeds. Locate the NodeRow component (export
const NodeRow = memo(...)) and update the calling code to memoize complex
objects (node, action, keyComponent, nsComponent) with useMemo and callbacks
(onClick) with useCallback, or alternatively pass primitive/stable identifiers
instead of recreated objects to make memo() actually prevent re-renders.
src/ui/views/Push/Changes.tsx (1)

27-36: Consider the performance impact of minVisibleRows={Infinity}.

Setting minVisibleRows to Infinity disables row virtualization, meaning all items will render at once. This is fine for small lists, but if there are many new/changed keys, it could degrade performance. If large diffs are possible, consider keeping virtualization or adding a reasonable upper bound.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/Push/Changes.tsx` around lines 27 - 36, The NodeList usage is
disabling virtualization by setting minVisibleRows={Infinity}; change it to
preserve virtualization for large lists by using a finite cap or conditional
logic based on changes.newKeys.length (e.g., remove the prop or set
minVisibleRows to Math.min(changes.newKeys.length, <reasonableLimit>) or only
set Infinity for very small lists). Update the NodeList prop handling where
NodeList is rendered and the items are derived from changes.newKeys to use this
conditional/finite limit.
src/ui/views/CopyView/CopyView.tsx (1)

45-49: Optional: Consider explicitly passing hasNamespacesEnabled for consistency.

The getPullChanges function accepts an optional hasNamespacesEnabled parameter (defaults to true). While Pull.tsx explicitly passes this parameter, both CopyView.tsx and CreateCopy.tsx rely on the default value. If conditional namespace behavior is needed here, pass the parameter explicitly; otherwise, the current approach is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/CopyView/CopyView.tsx` around lines 45 - 49, The call to
getPullChanges currently relies on its default hasNamespacesEnabled value;
explicitly pass the boolean to make behavior consistent with Pull.tsx (and match
CreateCopy.tsx if intended). Update the invocation where changedNodes is
computed (getPullChanges(connectedNodes.items, language!, translations)) to
include the desired hasNamespacesEnabled argument (true or false) so the
function signature getPullChanges(..., hasNamespacesEnabled) is explicit and
consistent across CopyView.tsx and CreateCopy.tsx.
src/main/endpoints/formatText.ts (1)

67-77: This cache still resets once per node update.

The cachedAvailableFonts variable is scoped to each formatText invocation, so batch updates via updateNodes call figma.listAvailableFontsAsync() once per node instead of once total.

Hoist the cache outside formatText and cache the promise to avoid redundant calls across the batch:

Suggested fix
+let cachedAvailableFontsPromise: Promise<Font[]> | null = null;
+
 export const formatText = async ({
   nodeInfo,
   formatted,
 }: FormatTextEndpointArgs) => {
...
-  let cachedAvailableFonts: Font[] | null = null;
   const getAvailableFonts = async () => {
-    if (!cachedAvailableFonts) {
-      cachedAvailableFonts = await figma.listAvailableFontsAsync();
+    if (!cachedAvailableFontsPromise) {
+      cachedAvailableFontsPromise = figma.listAvailableFontsAsync();
     }
-    return cachedAvailableFonts;
+    return cachedAvailableFontsPromise;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/endpoints/formatText.ts` around lines 67 - 77, cachedAvailableFonts
is currently declared inside formatText so each invocation (e.g., per node in
updateNodes) recreates the cache and triggers figma.listAvailableFontsAsync
repeatedly; move the cachedAvailableFonts variable and the
getAvailableFonts/getFontsOfFamily helpers out of formatText to module scope and
have cachedAvailableFonts store the Promise returned by
figma.listAvailableFontsAsync (or the resolved array) so concurrent calls reuse
the same in-flight request; update references in formatText to use the hoisted
cachedAvailableFonts/getAvailableFonts/getFontsOfFamily accordingly.
src/ui/views/Index/Index.tsx (1)

98-100: Empty dependency array may cause stale closures.

handleRefreshNamespaces uses namespacesLoadable.refetch and allNodes.refetch but has an empty dependency array. While react-query's refetch functions are typically stable, explicitly including them would be safer and more maintainable.

♻️ Consider adding dependencies
   const handleRefreshNamespaces = useCallback(async () => {
     await Promise.all([namespacesLoadable.refetch(), allNodes.refetch()]);
-  }, []);
+  }, [namespacesLoadable.refetch, allNodes.refetch]);

Alternatively, if the intent is to avoid re-renders, document why the empty array is safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/Index/Index.tsx` around lines 98 - 100, handleRefreshNamespaces
currently has an empty dependency array which can lead to stale closures for
namespacesLoadable.refetch and allNodes.refetch; update the useCallback
dependency array to include namespacesLoadable.refetch and allNodes.refetch (or
namespacesLoadable and allNodes if those are the stable objects you track) so
the callback always references the latest refetch functions, or add a short
comment explaining why an empty array is deliberately safe if you confirm those
refetch functions are stable.
src/ui/hooks/useHasBranchingEnabled.ts (1)

13-24: Consider using a safer fallback for projectId.

When apiKeyInfo.data?.projectId is undefined, the fallback value 0 is passed to the path. While the enabled condition prevents the query from running in this case, using a more explicit approach could improve clarity and safety against potential race conditions.

♻️ Optional: Use type assertion or explicit typing
   const projectQuery = useApiQuery({
     url: "/v2/projects/{projectId}",
     method: "get",
     path: {
-      projectId: apiKeyInfo.data?.projectId ?? 0,
+      projectId: apiKeyInfo.data?.projectId as number,
     },
     options: {
       cacheTime: 60000,
       staleTime: 60000,
       enabled: apiKeyInfo.data?.projectId !== undefined,
     },
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/hooks/useHasBranchingEnabled.ts` around lines 13 - 24, The path
fallback of 0 for projectId can hide bugs and is misleading because the query is
disabled when apiKeyInfo.data?.projectId is undefined; update the useApiQuery
call in useHasBranchingEnabled so the path.projectId is not forced to 0 but
instead uses the actual possibly-undefined value (e.g., projectId:
apiKeyInfo.data?.projectId) or an explicit typed assertion (e.g., as number |
undefined) so you don't pass a sentinel 0; keep the existing enabled check
(apiKeyInfo.data?.projectId !== undefined) to prevent execution when projectId
is absent.
src/ui/components/BranchSelect/BranchSelect.tsx (1)

44-51: Consider simplifying the nested setTimeout focus logic.

The double-nested setTimeout (100ms + 50ms) for focus restoration appears to be a workaround for timing issues after refresh. While it works, this approach is fragile and may behave inconsistently across different scenarios.

Consider using a single setTimeout with a slightly longer delay, or investigate if requestAnimationFrame could provide more reliable timing after the DOM updates.

♻️ Suggested simplification
       await onRefresh();
       if (wasFocused && inputRef.current) {
-        setTimeout(() => {
-          inputRef.current?.focus();
-          setTimeout(() => {
-            inputRef.current?.focus();
-          }, 50);
-        }, 100);
+        // Allow DOM to settle after refresh before restoring focus
+        requestAnimationFrame(() => {
+          requestAnimationFrame(() => {
+            inputRef.current?.focus();
+          });
+        });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/BranchSelect/BranchSelect.tsx` around lines 44 - 51, The
nested setTimeout calls used to restore focus (checking wasFocused and
inputRef.current) are fragile; replace them with a single delayed focus attempt
or use requestAnimationFrame to wait for DOM updates: when wasFocused is true,
schedule one reliable focus call on inputRef.current (either via a single
setTimeout with a slightly longer delay replacing the 100ms+50ms nesting, or by
using requestAnimationFrame (or two successive requestAnimationFrame calls) to
ensure rendering is complete before calling focus). Update the BranchSelect
logic that references wasFocused, inputRef, and the current setTimeout nesting
to perform one deterministic focus attempt.
src/ui/views/Settings/ProjectSettings.tsx (1)

220-228: Verify edge case: effect may not trigger if settings.branch is an empty string.

The condition !settings.branch would be true for both undefined and "" (empty string). If a branch could legitimately have an empty name, this effect would incorrectly try to set the default branch. However, empty branch names are unlikely in practice.

♻️ More explicit check for undefined
   useEffect(() => {
-    if (settings && !settings.branch && branches.length > 0) {
+    if (settings && settings.branch === undefined && branches.length > 0) {
       const defaultBranch = branches.find((b) => b.isDefault)?.name;
       if (defaultBranch) {
         setSettings((prev) => ({ ...prev!, branch: defaultBranch }));
       }
     }
   }, [branches, settings?.branch]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/Settings/ProjectSettings.tsx` around lines 220 - 228, The effect
in ProjectSettings.tsx checks `!settings.branch`, which treats empty string as
missing; change the condition to explicitly test for undefined (e.g.,
`settings.branch === undefined` or `settings.branch == null` if you also want to
catch null) inside the useEffect so only truly absent branches trigger assigning
the default; update the useEffect that references `branches`,
`settings?.branch`, and calls `setSettings` to use this explicit check.
src/ui/client/apiSchema.generated.ts (1)

1613-1621: Mirror the request-level branch shape on the deprecated resolvable import.

SingleStepImportResolvableRequest puts branch in the right place, but ImportKeysResolvableItemDto still puts it on each key. That forces callers to repeat the same branch on every item and permits mixed-branch payloads in one request. Since this file is generated, I'd fix this in the upstream OpenAPI schema.

♻️ Cleaner contract shape
   ImportKeysResolvableDto: {
+    branch?: string;
     keys: components["schemas"]["ImportKeysResolvableItemDto"][];
   };

   ImportKeysResolvableItemDto: {
       name: string;
-      branch?: string;
       namespace?: string;

Also applies to: 2871-2879

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/client/apiSchema.generated.ts` around lines 1613 - 1621, The generated
DTO places branch on each ImportKeysResolvableItemDto which forces per-item
repetition and allows mixed branches; update the OpenAPI schema (not just this
file) so branch is defined at the request-level (the same shape as in
SingleStepImportResolvableRequest) and remove the branch property from
ImportKeysResolvableItemDto (also remove the duplicate branch occurrences around
the other block noted near ImportKeysResolvableItemDto at ~2871-2879);
regenerate the client so ImportKeysResolvableItemDto no longer contains branch
and the request object carries the branch field consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eslint.config.js`:
- Around line 25-42: The config references the rule
"react-hooks/exhaustive-deps" but never registers the react-hooks plugin,
causing an unknown rule error; add the react-hooks plugin to the plugin imports
and register it in the plugins object (similar to tsPlugin/reactPlugin) — e.g.,
create or import a reactHooksPlugin variable and add reactHooks:
reactHooksPlugin to the plugins map in eslint.config.js so the
"react-hooks/exhaustive-deps" rule is valid (alternatively remove the
"react-hooks/exhaustive-deps" entry if you prefer not to use that plugin).

In `@package.json`:
- Around line 34-42: Update package.json to ensure ESLint v9 and Prettier v3 are
compatible: run the project linting/formatting locally and in CI using the
current "eslint" and "prettier" versions, fix or pin any incompatible ESLint
configs/plugins (e.g., "eslint-config-preact", "eslint-plugin-prettier",
"eslint-plugin-react") or Prettier integrations if they break, and adjust
package.json to pin working minor/patch ranges or add compatibility wrappers;
also confirm and add "scheduler" as a dependency to satisfy the peer requirement
of "use-context-selector" (>=0.19.0) if that package is present.

In `@src/main/endpoints/preformatKey.ts`:
- Around line 25-36: The replacement builds a RegExp from
separatorBeforePlaceholder which can contain regex metacharacters (from user
keyFormat) and thus may match incorrectly; in the function handling
preformatting (where originalFormat, placeholder, separatorBeforePlaceholder and
newFormat.replace are used) escape regex-special characters in
separatorBeforePlaceholder before passing it into new RegExp (use a utility or
inline escape that replaces characters like . * + ? ^ $ { } ( ) | [ ] \ / with
backslash-escaped versions) so the pattern matches the literal separator and
replacements behave correctly.

In `@src/ui/client/apiSchema.generated.ts`:
- Around line 5348-5353: The three generated operation types store_3,
executeComplexTagOperation_1, and getUsedNamespaces_1 currently declare
parameters? which allows undefined and breaks RequestParamsType expectations;
update each operation's type so parameters is required (parameters: { query?: {
... } }) while leaving the inner query optional, i.e. remove the optional marker
from the parameters wrapper for those three symbols so callers can safely access
parameters as assumed by the API client and types.ts.

In `@src/ui/hooks/useSelectedNodes.ts`:
- Around line 16-19: The debouncedRefetch callback references result.refetch but
does not include it in the useCallback dependency array, which can be incorrect
if a new query instance is created; update the debouncedRefetch useCallback to
include result.refetch in its dependency array (or explicitly document why it's
omitted) and ensure refetchTimerRef and any other values used inside
(refetchTimerRef, result.refetch) are accounted for so the memoized callback
stays correct; locate the debouncedRefetch declaration in useSelectedNodes.ts
and modify its dependency array accordingly.

In `@src/ui/views/CreateCopy/CreateCopy.tsx`:
- Around line 65-71: The loop currently calls copyPageMutation.mutate with
onSuccess: goToIndex which causes navigation as soon as the first mutation
succeeds; change to use copyPageMutation.mutateAsync (or the mutation's async
API) inside the loop to collect each mutation promise, then await
Promise.all(promises) before calling goToIndex so all copy operations complete
(or fail) before navigation; wrap the await in a try/catch to surface errors to
the user if any mutation rejects.

In `@src/ui/views/Pull/Pull.tsx`:
- Around line 38-43: The effect that calls computeDiff() currently only depends
on selectedNodes.data and lang, but computeDiff also reads branch and
hasNamespacesEnabled; update the effect dependency array to include branch and
hasNamespacesEnabled so the effect re-runs when the branch or namespace setting
changes (locate the useEffect that invokes computeDiff and add branch and
hasNamespacesEnabled to its dependency list alongside selectedNodes.data and
lang).

---

Outside diff comments:
In `@src/ui/components/Dialog/Dialog.tsx`:
- Around line 13-22: The effect currently reads ref.current both when adding the
keydown listener and again during cleanup, which can lead to a stale/different
element being used; capture the element into a local constant (e.g., const el =
ref.current) at the start of the useEffect, call el?.focus() and
el?.addEventListener("keydown", handler), and use the same el in the cleanup to
removeEventListener; keep the dependency on onClose and reference handler within
the effect so the correct listener is removed.

In `@src/ui/components/Popover/Popover.tsx`:
- Around line 63-75: The cleanup currently reads popoverTrigger.current at
teardown which can be null or changed; inside the useEffect that registers
listeners (useEffect referencing popoverTrigger, open, computePositionAndSet)
capture the current element into a local const (e.g., const trigger =
popoverTrigger.current) before adding listeners and then remove listeners from
that captured trigger in the return cleanup; do the same for any other refs used
(dropdownRef if you later use it) so the removeEventListener calls target the
same element instance that addEventListener was called on.

In `@src/ui/hooks/useInterpolatedTranslation.ts`:
- Around line 149-158: The effect currently strips HTML tags from the candidate
translation before comparing to previousCharacters, which causes the ref to drop
Tolgee markup; instead, call getOrUpdateInterpolatedTranslation once into a
variable (e.g., newWithMarkup), derive a stripped version only for the equality
check (newStripped = newWithMarkup?.replace(/<[^>]*>/g, "")), and if newStripped
is non-null and differs from previousCharacters then assign
interpolatedTranslation.current = newWithMarkup so the markup is preserved;
update references to useEffect, getOrUpdateInterpolatedTranslation,
interpolatedTranslation.current, previousCharacters, rawTranslation and
node?.isPlural accordingly.

In `@src/ui/views/CreateCopy/CreateCopy.tsx`:
- Around line 55-57: The call to allTranslationsLoadable.getData in
CreateCopy.tsx is missing the branch parameter so translations for non-default
branches are not fetched; update the call to pass the currently selected branch
(e.g., the same branch variable/prop used by the Pull component) by adding
branch: activeBranch (or the existing branch identifier in scope) to the getData
argument object, ensuring CreateCopy fetches translations from the correct
branch.

In `@src/ui/views/StringDetails/StringDetails.tsx`:
- Around line 439-457: TextboxNumeric's onChange prop is unsupported; replace it
with onValueInput or onInput and adapt the handler: move the logic that sets
setNeedsSubmission(true), updates currentNode.pluralParamValue and builds
newNode (NodeInfo) into an onValueInput={(value: string) => { ... }} (or onInput
to access event.currentTarget.value) and call setCurrentNode(newNode) only when
translation != null; ensure you keep the same checks for empty string and
preserve setting currentNode.pluralParamValue, setNeedsSubmission, and
construction of newNode so the behavior of TextboxNumeric, currentNode,
setCurrentNode, setNeedsSubmission and translation remains identical.

---

Nitpick comments:
In `@src/main/endpoints/formatText.ts`:
- Around line 67-77: cachedAvailableFonts is currently declared inside
formatText so each invocation (e.g., per node in updateNodes) recreates the
cache and triggers figma.listAvailableFontsAsync repeatedly; move the
cachedAvailableFonts variable and the getAvailableFonts/getFontsOfFamily helpers
out of formatText to module scope and have cachedAvailableFonts store the
Promise returned by figma.listAvailableFontsAsync (or the resolved array) so
concurrent calls reuse the same in-flight request; update references in
formatText to use the hoisted
cachedAvailableFonts/getAvailableFonts/getFontsOfFamily accordingly.

In `@src/main/utils/nodeParents.ts`:
- Around line 52-80: The helper accessors (getFrame, getArtboard, getComponent,
getSection, getGroup) each call getAllParents individually which can re-traverse
the parent chain and cause a performance regression when multiple helpers are
used for the same node; update the JSDoc for these functions (and optionally the
file header) to briefly note this trade-off and recommend callers call
getAllParents(nodeId) once and reuse its returned
.frame/.artboard/.component/.section/.group when they need multiple parent types
for the same node.

In `@src/ui/client/apiSchema.generated.ts`:
- Around line 1613-1621: The generated DTO places branch on each
ImportKeysResolvableItemDto which forces per-item repetition and allows mixed
branches; update the OpenAPI schema (not just this file) so branch is defined at
the request-level (the same shape as in SingleStepImportResolvableRequest) and
remove the branch property from ImportKeysResolvableItemDto (also remove the
duplicate branch occurrences around the other block noted near
ImportKeysResolvableItemDto at ~2871-2879); regenerate the client so
ImportKeysResolvableItemDto no longer contains branch and the request object
carries the branch field consistently.

In `@src/ui/components/BranchSelect/BranchSelect.tsx`:
- Around line 44-51: The nested setTimeout calls used to restore focus (checking
wasFocused and inputRef.current) are fragile; replace them with a single delayed
focus attempt or use requestAnimationFrame to wait for DOM updates: when
wasFocused is true, schedule one reliable focus call on inputRef.current (either
via a single setTimeout with a slightly longer delay replacing the 100ms+50ms
nesting, or by using requestAnimationFrame (or two successive
requestAnimationFrame calls) to ensure rendering is complete before calling
focus). Update the BranchSelect logic that references wasFocused, inputRef, and
the current setTimeout nesting to perform one deterministic focus attempt.

In `@src/ui/components/NodeList/NodeRow.tsx`:
- Around line 21-30: NodeRow is wrapped with memo() but memoization will be
ineffective if parent repeatedly recreates props; ensure stable references for
props like node, action, keyComponent, nsComponent, onClick (or wrap them with
useMemo/useCallback) in the parent so shallow comparison succeeds. Locate the
NodeRow component (export const NodeRow = memo(...)) and update the calling code
to memoize complex objects (node, action, keyComponent, nsComponent) with
useMemo and callbacks (onClick) with useCallback, or alternatively pass
primitive/stable identifiers instead of recreated objects to make memo()
actually prevent re-renders.

In `@src/ui/hooks/useHasBranchingEnabled.ts`:
- Around line 13-24: The path fallback of 0 for projectId can hide bugs and is
misleading because the query is disabled when apiKeyInfo.data?.projectId is
undefined; update the useApiQuery call in useHasBranchingEnabled so the
path.projectId is not forced to 0 but instead uses the actual possibly-undefined
value (e.g., projectId: apiKeyInfo.data?.projectId) or an explicit typed
assertion (e.g., as number | undefined) so you don't pass a sentinel 0; keep the
existing enabled check (apiKeyInfo.data?.projectId !== undefined) to prevent
execution when projectId is absent.

In `@src/ui/styles.css`:
- Around line 49-50: The current CSS rule that sets "overflow-x: hidden" and
"text-overflow: ellipsis" needs "white-space: nowrap" added to ensure the
ellipsis consistently appears; update the same selector (the rule containing
overflow-x and text-overflow in src/ui/styles.css) to include white-space:
nowrap, and if this targets native <select> elements note their text-overflow
support is inconsistent across browsers and consider using a custom styled
control if cross-browser truncation is required.

In `@src/ui/views/CopyView/CopyView.tsx`:
- Around line 45-49: The call to getPullChanges currently relies on its default
hasNamespacesEnabled value; explicitly pass the boolean to make behavior
consistent with Pull.tsx (and match CreateCopy.tsx if intended). Update the
invocation where changedNodes is computed (getPullChanges(connectedNodes.items,
language!, translations)) to include the desired hasNamespacesEnabled argument
(true or false) so the function signature getPullChanges(...,
hasNamespacesEnabled) is explicit and consistent across CopyView.tsx and
CreateCopy.tsx.

In `@src/ui/views/Index/Index.tsx`:
- Around line 98-100: handleRefreshNamespaces currently has an empty dependency
array which can lead to stale closures for namespacesLoadable.refetch and
allNodes.refetch; update the useCallback dependency array to include
namespacesLoadable.refetch and allNodes.refetch (or namespacesLoadable and
allNodes if those are the stable objects you track) so the callback always
references the latest refetch functions, or add a short comment explaining why
an empty array is deliberately safe if you confirm those refetch functions are
stable.

In `@src/ui/views/Push/Changes.tsx`:
- Around line 27-36: The NodeList usage is disabling virtualization by setting
minVisibleRows={Infinity}; change it to preserve virtualization for large lists
by using a finite cap or conditional logic based on changes.newKeys.length
(e.g., remove the prop or set minVisibleRows to Math.min(changes.newKeys.length,
<reasonableLimit>) or only set Infinity for very small lists). Update the
NodeList prop handling where NodeList is rendered and the items are derived from
changes.newKeys to use this conditional/finite limit.

In `@src/ui/views/Settings/ProjectSettings.tsx`:
- Around line 220-228: The effect in ProjectSettings.tsx checks
`!settings.branch`, which treats empty string as missing; change the condition
to explicitly test for undefined (e.g., `settings.branch === undefined` or
`settings.branch == null` if you also want to catch null) inside the useEffect
so only truly absent branches trigger assigning the default; update the
useEffect that references `branches`, `settings?.branch`, and calls
`setSettings` to use this explicit check.

In `@src/ui/views/StringDetails/StringDetails.tsx`:
- Around line 460-464: The filter predicate in the StringDetails component uses
loose inequality when comparing p.name and tolgeeValue.parameter; update the
comparison in the lambda passed to .filter (the function referencing
tolgeeValue.parameter and p.name) to use strict inequality !== instead of != to
avoid coercion and make intent explicit.

In `@src/utilities.ts`:
- Around line 28-57: The switch branches for "snake_case" and
"snake_case_capitalized" are missing trailing commas on their .map(...) arrow
function expressions, causing inconsistency with the "camelCase" and
"PascalCase" branches; update the .map callbacks inside the "snake_case" and
"snake_case_capitalized" cases to include trailing commas (same style as used in
the "camelCase" and "PascalCase" branches) so all map operations are
consistently formatted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f0e6ede-2bb3-4490-b352-f82dc4f6c909

📥 Commits

Reviewing files that changed from the base of the PR and between cad63ff and f6e2b06.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (82)
  • eslint.config.js
  • package.json
  • src/createFormatIcu.ts
  • src/main/endpoints/copyPage.ts
  • src/main/endpoints/editorType.ts
  • src/main/endpoints/formatText.ts
  • src/main/endpoints/getScreenshots.ts
  • src/main/endpoints/highlightNode.ts
  • src/main/endpoints/preformatKey.ts
  • src/main/endpoints/setNodesData.ts
  • src/main/endpoints/updateNodes.ts
  • src/main/main.ts
  • src/main/utils/createEndpoint.ts
  • src/main/utils/delayed.ts
  • src/main/utils/nodeParents.ts
  • src/main/utils/nodeTools.ts
  • src/main/utils/pages.ts
  • src/main/utils/settingsTools.ts
  • src/main/utils/textFormattingTools.ts
  • src/tools/createProvider.tsx
  • src/tools/getConflictingNodes.ts
  • src/tools/getPullChanges.ts
  • src/tools/getPushChanges.ts
  • src/types.ts
  • src/ui/Plugin.tsx
  • src/ui/client/apiSchema.custom.ts
  • src/ui/client/apiSchema.generated.ts
  • src/ui/client/client.ts
  • src/ui/client/decodeApiKey.ts
  • src/ui/client/types.ts
  • src/ui/client/useQueryApi.ts
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx
  • src/ui/components/BranchSelect/BranchSelect.css
  • src/ui/components/BranchSelect/BranchSelect.css.d.ts
  • src/ui/components/BranchSelect/BranchSelect.tsx
  • src/ui/components/Dialog/Dialog.tsx
  • src/ui/components/Editor/Editor.tsx
  • src/ui/components/Editor/TranslationPlurals.tsx
  • src/ui/components/KeyOptionsButton/KeyOptionsButton.tsx
  • src/ui/components/NamespaceSelect/NamespaceSelect.tsx
  • src/ui/components/NodeList/NodeList.tsx
  • src/ui/components/NodeList/NodeRow.tsx
  • src/ui/components/Popover/Popover.tsx
  • src/ui/components/ResizeHandle/ResizeHandle.tsx
  • src/ui/components/Shared/HtmlText.tsx
  • src/ui/hooks/useAllTranslations.ts
  • src/ui/hooks/useConnectedMutation.ts
  • src/ui/hooks/useConnectedNodes.ts
  • src/ui/hooks/useCopyPage.ts
  • src/ui/hooks/useEditorMode.ts
  • src/ui/hooks/useFigmaNotify.ts
  • src/ui/hooks/useFormatText.ts
  • src/ui/hooks/useHasBranchingEnabled.ts
  • src/ui/hooks/useHasNamespacesEnabled.ts
  • src/ui/hooks/useHighlightNodeMutation.ts
  • src/ui/hooks/useInterpolatedTranslation.ts
  • src/ui/hooks/usePrefilledKey.ts
  • src/ui/hooks/useSelectedNodes.ts
  • src/ui/hooks/useSetNodesDataMutation.ts
  • src/ui/hooks/useTranslation.ts
  • src/ui/hooks/useUpdateNodesMutation.ts
  • src/ui/hooks/useWindowSize.ts
  • src/ui/state/GlobalState.ts
  • src/ui/styles.css
  • src/ui/views/Connect/Connect.tsx
  • src/ui/views/CopyView/CopyView.tsx
  • src/ui/views/CreateCopy/CreateCopy.tsx
  • src/ui/views/Index/Index.tsx
  • src/ui/views/Index/KeyInput.tsx
  • src/ui/views/Index/ListItem.tsx
  • src/ui/views/Pull/Pull.tsx
  • src/ui/views/Push/Changes.tsx
  • src/ui/views/Push/Push.tsx
  • src/ui/views/Settings/ProjectSettings.tsx
  • src/ui/views/Settings/PushSection.tsx
  • src/ui/views/Settings/Settings.tsx
  • src/ui/views/Settings/StringsEditor.tsx
  • src/ui/views/Settings/StringsSection.tsx
  • src/ui/views/StringDetails/StringDetails.tsx
  • src/utilities.ts
  • src/web/main.ts
  • tsconfig.json
💤 Files with no reviewable changes (3)
  • src/ui/components/Shared/HtmlText.tsx
  • src/ui/hooks/useHasNamespacesEnabled.ts
  • src/ui/views/Settings/PushSection.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ui/views/CreateCopy/CreateCopy.tsx (1)

48-70: ⚠️ Potential issue | 🟠 Major

Wrap async operations in try/catch to handle mutation failures.

The refactor to mutateAsync is correct, but if any mutation throws (e.g., Figma API errors in copyPageEndpoint, network issues), the error propagates uncaught, goToIndex() never executes, and the user is stuck on the page with no feedback.

🛡️ Proposed fix to add error handling
 const handleSubmit = async () => {
+  try {
     if (copyType === "keys") {
       await copyPageMutation.mutateAsync(undefined);
     } else {
       for (const language of selectedLanguages) {
         const response = await allTranslationsLoadable.getData({
           language,
         });

         const { changedNodes } = getPullChanges(
           connectedNodes.data?.items || [],
           language,
           response,
         );

         await copyPageMutation.mutateAsync({
           language,
           nodes: changedNodes,
         });
       }
     }
     goToIndex();
+  } catch (error) {
+    console.error("Failed to create page copy:", error);
+    // Consider using a toast/notification to inform the user
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/CreateCopy/CreateCopy.tsx` around lines 48 - 70, handleSubmit
currently awaits mutateAsync calls without error handling; wrap the main async
work in a try/catch (using handleSubmit) so any thrown error from
copyPageMutation.mutateAsync or allTranslationsLoadable.getData is caught, call
your existing error reporting (e.g., setError / toast / logger) in the catch
with the caught error, and ensure goToIndex() is invoked appropriately (either
in a finally block if you want to always navigate, or after successful
completion in the try block) so the user is not left stuck; reference
handleSubmit, copyPageMutation, allTranslationsLoadable.getData, getPullChanges,
selectedLanguages, connectedNodes, and goToIndex when making the change.
src/main/endpoints/preformatKey.ts (1)

27-44: ⚠️ Potential issue | 🟡 Minor

Handle empty placeholders at the start of keyFormat.

On Line 34, this branch only strips the separator before an empty placeholder. If the first token is missing, {section}.{elementName} still formats to .title because the . after {section} is untouched. Please cover the leading-placeholder case as well so optional prefixes don’t leave dangling separators.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/endpoints/preformatKey.ts` around lines 27 - 44, The branch that
handles empty replaceString only removes the separator before a missing
placeholder, causing a leading empty token to leave a dangling separator; update
the logic in preformatKey to also detect when the placeholder is at the start
(use originalFormat.indexOf(placeholder) === 0 or compute
separatorAfterPlaceholder from
originalFormat.slice(originalFormat.indexOf(placeholder) + placeholder.length,
originalFormat.indexOf(placeholder) + placeholder.length + 1)) and remove the
placeholder plus the following separator as well; adjust the RegExp logic (using
escapeRegExp, placeholder and either separatorBeforePlaceholder or
separatorAfterPlaceholder) so both "separator+placeholder" and
"placeholder+separator" cases are stripped from newFormat when replaceString ===
"".
🧹 Nitpick comments (1)
src/ui/views/Pull/Pull.tsx (1)

106-112: Consider logging formatting errors in development.

While the fallback to n.characters is reasonable, silently swallowing all formatting errors could mask issues with malformed ICU messages or parameter mismatches. A conditional console.warn in development could help debugging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/views/Pull/Pull.tsx` around lines 106 - 112, The catch block in
Pull.tsx that currently swallows formatting errors should log the error in
development to aid debugging: inside the catch(e) for the formatting attempt
that returns "{ ...n, formatted: n.characters } as NodeInfo & { formatted:
string }", add a conditional development-only warning (e.g. if
(process.env.NODE_ENV !== 'production') console.warn(...)) that includes the
caught error `e` and identifying info from `n` (such as `n.id` or
`n.characters`) so you still return the fallback but surface formatting/ICU
issues during development.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/main/endpoints/preformatKey.ts`:
- Around line 27-44: The branch that handles empty replaceString only removes
the separator before a missing placeholder, causing a leading empty token to
leave a dangling separator; update the logic in preformatKey to also detect when
the placeholder is at the start (use originalFormat.indexOf(placeholder) === 0
or compute separatorAfterPlaceholder from
originalFormat.slice(originalFormat.indexOf(placeholder) + placeholder.length,
originalFormat.indexOf(placeholder) + placeholder.length + 1)) and remove the
placeholder plus the following separator as well; adjust the RegExp logic (using
escapeRegExp, placeholder and either separatorBeforePlaceholder or
separatorAfterPlaceholder) so both "separator+placeholder" and
"placeholder+separator" cases are stripped from newFormat when replaceString ===
"".

In `@src/ui/views/CreateCopy/CreateCopy.tsx`:
- Around line 48-70: handleSubmit currently awaits mutateAsync calls without
error handling; wrap the main async work in a try/catch (using handleSubmit) so
any thrown error from copyPageMutation.mutateAsync or
allTranslationsLoadable.getData is caught, call your existing error reporting
(e.g., setError / toast / logger) in the catch with the caught error, and ensure
goToIndex() is invoked appropriately (either in a finally block if you want to
always navigate, or after successful completion in the try block) so the user is
not left stuck; reference handleSubmit, copyPageMutation,
allTranslationsLoadable.getData, getPullChanges, selectedLanguages,
connectedNodes, and goToIndex when making the change.

---

Nitpick comments:
In `@src/ui/views/Pull/Pull.tsx`:
- Around line 106-112: The catch block in Pull.tsx that currently swallows
formatting errors should log the error in development to aid debugging: inside
the catch(e) for the formatting attempt that returns "{ ...n, formatted:
n.characters } as NodeInfo & { formatted: string }", add a conditional
development-only warning (e.g. if (process.env.NODE_ENV !== 'production')
console.warn(...)) that includes the caught error `e` and identifying info from
`n` (such as `n.id` or `n.characters`) so you still return the fallback but
surface formatting/ICU issues during development.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 469d7fcc-4fa4-45d3-a811-14e96f89d0bd

📥 Commits

Reviewing files that changed from the base of the PR and between f6e2b06 and a1ad15d.

📒 Files selected for processing (6)
  • eslint.config.js
  • src/main/endpoints/preformatKey.ts
  • src/ui/components/Dialog/Dialog.tsx
  • src/ui/hooks/useSelectedNodes.ts
  • src/ui/views/CreateCopy/CreateCopy.tsx
  • src/ui/views/Pull/Pull.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ui/components/Dialog/Dialog.tsx
  • src/ui/hooks/useSelectedNodes.ts
  • eslint.config.js

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/ui/hooks/useHasBranchingEnabled.ts (1)

27-38: Consider removing or gating the console.info for production.

While the log is helpful during development/debugging, it will emit on every project load in production. Consider gating it behind an environment check or removing it entirely if the information is not essential for end users.

- useEffect(() => {
-   if (projectQuery.data) {
-     // eslint-disable-next-line no-console
-     console.info(
-       `[Tolgee] Branching feature: ${hasBranching ? "ENABLED" : "DISABLED — enable it in your Tolgee project settings (Settings → General → Branching)"}`
-     );
-   }
- }, [projectQuery.data, hasBranching]);
+ useEffect(() => {
+   if (projectQuery.data && process.env.NODE_ENV === 'development') {
+     // eslint-disable-next-line no-console
+     console.info(
+       `[Tolgee] Branching feature: ${hasBranching ? "ENABLED" : "DISABLED — enable it in your Tolgee project settings (Settings → General → Branching)"}`
+     );
+   }
+ }, [projectQuery.data, hasBranching]);

Alternatively, if this log is intentionally user-facing, consider keeping it as-is but be aware it runs on every mount where project data is available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/hooks/useHasBranchingEnabled.ts` around lines 27 - 38, The
console.info in useHasBranchingEnabled (inside the useEffect that reads
projectQuery.data and hasBranching) should be removed or gated for production;
update the useEffect to either remove the console.info entirely or wrap the log
call in an environment check (e.g., only run when process.env.NODE_ENV !==
'production' or another debug flag) or replace it with a debug/logger utility so
it does not emit on every project load in production while still preserving the
existing behavior during development.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/ui/hooks/useHasBranchingEnabled.ts`:
- Around line 27-38: The console.info in useHasBranchingEnabled (inside the
useEffect that reads projectQuery.data and hasBranching) should be removed or
gated for production; update the useEffect to either remove the console.info
entirely or wrap the log call in an environment check (e.g., only run when
process.env.NODE_ENV !== 'production' or another debug flag) or replace it with
a debug/logger utility so it does not emit on every project load in production
while still preserving the existing behavior during development.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd89a154-d035-412c-88e0-dd1bf52fdc08

📥 Commits

Reviewing files that changed from the base of the PR and between a1ad15d and 0acd50e.

📒 Files selected for processing (2)
  • src/ui/hooks/useHasBranchingEnabled.ts
  • src/ui/views/Index/ListItem.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/views/Index/ListItem.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant