Skip to content

Conversation

@eweren
Copy link
Contributor

@eweren eweren commented Dec 17, 2025

Summary by CodeRabbit

  • New Features

    • Added AutocompleteSelect component for improved dropdown interactions with multi-select support and keyboard navigation.
    • Introduced namespace refresh capability with visual feedback.
    • Added dynamic namespace detection based on API project settings.
    • Enhanced error messaging for image uploads and API key validation.
  • Bug Fixes

    • Improved hidden layer detection logic to include parent visibility checks.
    • Optimized node deduplication from O(n²) to O(n) using Set-based approach.
    • Fixed namespace handling in push/pull operations.
  • UI/UX Improvements

    • Simplified namespace selector interface with refresh button.
    • Updated stacking order for better overlay management.
    • Enhanced dark mode color consistency.
  • Tests

    • Added tests for key count verification in push success messages.
    • Updated namespace selector test paths.
  • Chores

    • Updated API schema generation configuration and client typing extensions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

Walkthrough

This PR refactors namespace selection to use an AutocompleteSelect component instead of a modal, adds namespace-aware API queries, updates the push workflow to respect namespace settings, improves error handling, and introduces hidden-layer child traversal logic for node filtering.

Changes

Cohort / File(s) Summary
Test Updates
cypress/e2e/index.cy.ts, cypress/e2e/push.cy.ts
Updated namespace selector path to use general_namespace_select_input; removed "hides namespace selector" test. Added two new push tests for verifying key count in success messages for new and changed keys.
Configuration
package.json
Updated schema generation script: changed endpoint from localhost:8080 to localhost:8085 and modified output path from ./src/client/apiSchema.generated.ts to ./src/ui/client/apiSchema.generated.ts.
Core Logic
src/main/utils/nodeTools.ts, src/main/utils/settingsTools.ts, src/tools/getPushChanges.ts
Modified ignoreHiddenLayers block to always execute and added new ignoreHiddenLayersIncludingChildren traversal for parent visibility detection. Replaced namespacesDisabled with ignoreHiddenLayersIncludingChildren in settings. Changed getPushChanges signature from language: string to hasNamespacesEnabled: boolean and made namespace field conditional in output.
Type Definitions
src/types.ts
Added optional ignoreHiddenLayersIncludingChildren?: boolean to GlobalSettings; removed namespacesDisabled from CurrentDocumentSettings.
API & Client Layer
src/ui/client/apiSchema.custom.ts, src/ui/client/client.ts, src/ui/client/useQueryApi.ts
Created custom API schema for /v2/projects/{projectId} GET endpoint. Extended client and useQueryApi generic Paths to include customPaths; adjusted URL rewriting logic to only apply when pathParams.projectId is undefined.
New Component: AutocompleteSelect
src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx, src/ui/components/AutocompleteSelect/AutocompleteSelect.css, src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts
Introduced new AutocompleteSelect component with single and multi-select modes, keyboard interactions, dynamic dropdown placement, and real-time filtering. Includes CSS styling for dropdown, items, header, and action row.
NamespaceSelect Refactor
src/ui/components/NamespaceSelect/NamespaceSelect.tsx, src/ui/components/NamespaceSelect/NamespaceSelect.css, src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts
Replaced modal/select-based flow with AutocompleteSelect; added optional onRefresh callback and refresh button with rotating animation; aggregates namespaces from API and connected nodes.
Node UI Updates
src/ui/components/NodeList/NodeRow.tsx, src/ui/components/NodeList/NodeRow.css
Changed namespace label from "ns:" to "Namespace:"; updated CSS for overflow and spacing in .ns class.
Hooks
src/ui/hooks/useAllTranslations.ts, src/ui/hooks/useHasNamespacesEnabled.ts, src/ui/hooks/useSetNodesDataMutation.ts
Added clearCache() method to useAllTranslations; introduced new useHasNamespacesEnabled hook that queries current API key and project info to determine namespace support; added cache invalidation for getConnectedNodes after setNodesData mutation.
View Components: Index & ListItem
src/ui/views/Index/Index.tsx, src/ui/views/Index/ListItem.tsx, src/ui/views/Index/Index.css, src/ui/views/Index/Index.css.d.ts
Added useConnectedNodes and useHasNamespacesEnabled hooks; compute aggregated namespaces and refresh handler; pass hasNamespacesEnabled and onRefreshNamespaces to ListItem. Removed .nsSelect CSS class and editor-mode dependency; updated namespace rendering condition.
Push View
src/ui/views/Push/Push.tsx
Integrated useHasNamespacesEnabled to conditionally load translations; replaced O(n²) deduplication with O(n) Set-based approach; refactored diff computation into useEffect with namespace-aware loading; enhanced push flow with screenshot uploads, dynamic key resolution (NEW vs OVERRIDE), tagging, and improved error handling; display pushed key counts on success screen.
Pull View
src/ui/views/Pull/Pull.tsx
Extended error handling to cover "too_many_uploaded_images" error case with specific error message.
Settings Views
src/ui/views/Settings/ProjectSettings.tsx, src/ui/views/Settings/PushSection.tsx, src/ui/views/Settings/StringsSection.tsx
Replaced namespacesDisabled state with useHasNamespacesEnabled hook in ProjectSettings; added dynamic namespace help content with API links; sorted namespaces alphabetically. In PushSection, replaced multi-hook tag UI with AutocompleteSelect component. In StringsSection, added ignoreHiddenLayersIncludingChildren checkbox with help text and updated label styling.
Styling & Icons
src/ui/components/ActionsBottom/ActionsBottom.css, src/ui/components/InfoTooltip/InfoTooltip.css, src/ui/icons/SvgIcons.tsx, src/ui/styles.css, src/ui/views/Settings/StringsEditor.css
Increased z-index values for ActionsBottom and InfoTooltip components. Added Refresh SVG icon. Added dark-mode CSS variable --figma-color-text-primary: #ffffff``. Updated placeholder color in StringsEditor for dark mode.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Push View
    participant Hook as useHasNamespacesEnabled
    participant Query as useQueryApi
    participant API as API Client
    participant Compute as Diff Computation

    UI->>Hook: Check if namespaces enabled
    activate Hook
    Hook->>Query: Fetch /v2/api-keys/current
    Query->>API: GET /v2/api-keys/current
    API-->>Query: Return projectId from API key
    Query-->>Hook: Return projectId
    
    Hook->>Query: Fetch /v2/projects/{projectId}
    Query->>API: GET /v2/projects/{projectId}
    API-->>Query: Return project with useNamespaces flag
    Query-->>Hook: Return useNamespaces flag
    deactivate Hook
    Hook-->>UI: hasNamespacesEnabled

    alt Namespaces Enabled
        UI->>Compute: Load translations (with namespaces)
        Compute->>Query: Fetch translations by namespace
        Query-->>Compute: Return namespaced translations
    else Namespaces Disabled
        UI->>Compute: Load translations (default namespace only)
        Compute->>Query: Fetch default translations
        Query-->>Compute: Return default translations
    end

    UI->>Compute: Compute diff with node changes
    Compute->>Compute: getDiffChanges(nodes, translations, hasNamespacesEnabled)
    Compute-->>UI: Return KeyChanges with conditional ns field

    UI->>UI: Deduplicate nodes (Set-based O(n))
    UI->>UI: Prepare push payload (keys, screenshots, tags)
    UI->>API: POST /v2/projects/{id}/import (with translations)
    API-->>UI: Return result with imported key IDs
    
    UI->>UI: Process each key: resolve NEW vs OVERRIDE
    UI->>API: PUT /v2/keys/{keyId}/update-translations
    API-->>UI: Acknowledge update
    
    opt Tags enabled
        UI->>API: PUT /v2/keys/{keyId}/big-meta/related-keys
        API-->>UI: Confirm tag update
    end

    opt Screenshots enabled
        UI->>API: Upload screenshots with ArrayBuffer
        API-->>UI: Confirm upload
    end

    UI->>Query: Invalidate /getConnectedNodes cache
    Query-->>UI: Cache cleared
    
    UI->>UI: Show success screen with pushed key counts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • stepan662
  • ZuzanaOdstrcilova
  • JanCizmar

Poem

🐰 A namespace awakens with autocomplete's grace,
Hidden layers now traversing parent-node space!
Push flows dance with namespaces in tow,
Screenshots upload while the key counts glow—
From modal to select, a refactor so bright,
This PR hops forward with logic and light! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Fix namespaces issues' is vague and generic, using non-descriptive language that doesn't clearly convey the specific changes in the changeset. Replace with a more specific title that describes the primary change, such as 'Refactor namespace selection and visibility handling' or 'Replace namespace modal with autocomplete select UI'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

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
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 (1)
cypress/e2e/index.cy.ts (1)

32-53: Test logic is inconsistent after removing namespacesDisabled.

After removing namespacesDisabled: true from the config (line 35), this test now has identical configuration to the first test ({ ...SIGNED_IN }), yet expects opposite behavior:

  • Test 1 (line 8): config: SIGNED_IN → expects namespace selector to exist (lines 25-29)
  • Test 2 (line 35): config: { ...SIGNED_IN } → expects namespace selector to NOT exist (line 52)

Without a config setting to hide the namespace selector, the expectation on line 52 will fail. Please either:

  1. Update the assertion to expect the selector to exist, or
  2. Add a config setting that actually hides the namespace selector, or
  3. Remove this test if it's no longer relevant after the namespace refactor.
🧹 Nitpick comments (5)
src/ui/hooks/useAllTranslations.ts (1)

39-43: Consider simplifying the empty namespace handling logic.

When namespaces is provided (truthy), line 33 assigns nsNames directly from namespaces. This means if namespaces includes "", then nsNames will already include it, making the check on line 41 (!nsNames.includes("")) always true when needed. The defensive code appears correct but may be overly cautious.

Consider this simplification if the intent is to handle only the explicit namespaces parameter case:

-    // Ensure empty string is included if namespaces is provided and contains empty string
-    // This handles the default namespace case
-    if (namespaces && namespaces.includes("") && !nsNames.includes("")) {
-      nsNames.push("");
-    }
+    // Ensure empty string is included when explicitly requested
+    if (namespaces?.includes("") && !nsNames.includes("")) {
+      nsNames.push("");
+    }
src/ui/components/NamespaceSelect/NamespaceSelect.tsx (1)

51-58: Consider simplifying the nested setTimeout pattern.

The double focus() calls with nested timeouts (100ms then 50ms) appear fragile and may not reliably restore focus across different browsers or system loads.

Consider a single timeout with a reasonable delay, or use a more deterministic approach like listening for a state update callback:

       if (wasFocused && inputRef.current) {
-        setTimeout(() => {
-          inputRef.current?.focus();
-          // Small delay to ensure the options are updated
-          setTimeout(() => {
-            inputRef.current?.focus();
-          }, 50);
-        }, 100);
+        // Allow time for options to update before refocusing
+        setTimeout(() => inputRef.current?.focus(), 150);
       }
src/ui/client/useQueryApi.ts (1)

51-65: Consider extending MutationProps with customPaths for consistency.

The useApiQuery hook's Paths generic now includes customPaths (line 32), but MutationProps still defaults to only paths (line 54). If mutations need to support custom endpoints, update line 54 to match:

 export type MutationProps<
   Url extends keyof Paths,
   Method extends keyof Paths[Url],
-  Paths = paths
+  Paths = paths & customPaths
 > = {

If mutations are intentionally restricted to generated paths only, this is acceptable as-is.

src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx (1)

176-191: Consider reducing the blur delay timeout.

The 200ms delay in handleBlur (line 185) allows dropdown item clicks to register before closing, but it also delays the UI response. Consider:

  • Reducing to 100-150ms if 200ms feels sluggish
  • Using onMouseDown for dropdown items (which you already do) to avoid needing such a long delay
src/ui/views/Push/Push.tsx (1)

82-92: Consider including empty namespace in the dependency key.

The requiredNamespacesKey filters out empty/falsy namespaces on line 88, but the actual namespace computation inside the effect (line 127) includes empty strings via node.ns ?? "". While this works because the key is only for dependency tracking, aligning them would make the logic easier to follow.

     const uniqueNamespaces = Array.from(
       new Set(
         deduplicatedNodes
-          .map((node) => node.ns)
-          .filter((ns): ns is string => Boolean(ns))
+          .map((node) => node.ns ?? "")
       )
     );
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aaa074a and d3f8c04.

📒 Files selected for processing (36)
  • cypress/e2e/index.cy.ts (2 hunks)
  • cypress/e2e/push.cy.ts (1 hunks)
  • package.json (1 hunks)
  • src/main/utils/nodeTools.ts (1 hunks)
  • src/main/utils/settingsTools.ts (2 hunks)
  • src/tools/getPushChanges.ts (3 hunks)
  • src/types.ts (1 hunks)
  • src/ui/client/apiSchema.custom.ts (1 hunks)
  • src/ui/client/client.ts (3 hunks)
  • src/ui/client/useQueryApi.ts (2 hunks)
  • src/ui/components/ActionsBottom/ActionsBottom.css (1 hunks)
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.css (1 hunks)
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts (1 hunks)
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx (1 hunks)
  • src/ui/components/InfoTooltip/InfoTooltip.css (1 hunks)
  • src/ui/components/NamespaceSelect/NamespaceSelect.css (1 hunks)
  • src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts (1 hunks)
  • src/ui/components/NamespaceSelect/NamespaceSelect.tsx (1 hunks)
  • src/ui/components/NodeList/NodeRow.css (1 hunks)
  • src/ui/components/NodeList/NodeRow.tsx (1 hunks)
  • src/ui/hooks/useAllTranslations.ts (2 hunks)
  • src/ui/hooks/useHasNamespacesEnabled.ts (1 hunks)
  • src/ui/hooks/useSetNodesDataMutation.ts (1 hunks)
  • src/ui/icons/SvgIcons.tsx (1 hunks)
  • src/ui/styles.css (1 hunks)
  • src/ui/views/Index/Index.css (0 hunks)
  • src/ui/views/Index/Index.css.d.ts (0 hunks)
  • src/ui/views/Index/Index.tsx (4 hunks)
  • src/ui/views/Index/ListItem.tsx (3 hunks)
  • src/ui/views/Pull/Pull.tsx (1 hunks)
  • src/ui/views/Push/Push.tsx (10 hunks)
  • src/ui/views/Settings/ProjectSettings.tsx (6 hunks)
  • src/ui/views/Settings/PushSection.tsx (3 hunks)
  • src/ui/views/Settings/Settings.tsx (1 hunks)
  • src/ui/views/Settings/StringsEditor.css (1 hunks)
  • src/ui/views/Settings/StringsSection.tsx (4 hunks)
💤 Files with no reviewable changes (2)
  • src/ui/views/Index/Index.css.d.ts
  • src/ui/views/Index/Index.css
🧰 Additional context used
🧬 Code graph analysis (14)
src/ui/hooks/useSetNodesDataMutation.ts (3)
src/main/endpoints/setNodesData.ts (2)
  • SetNodesDataProps (5-7)
  • setNodesDataEndpoint (9-28)
src/main/utils/delayed.ts (1)
  • delayed (1-6)
src/main/endpoints/getConnectedNodes.ts (1)
  • getConnectedNodesEndpoint (9-22)
cypress/e2e/push.cy.ts (2)
src/web/urlConfig.ts (2)
  • createTestNode (82-93)
  • SIGNED_IN (39-45)
cypress/common/tools.ts (1)
  • visitWithState (5-9)
src/ui/hooks/useHasNamespacesEnabled.ts (1)
src/ui/client/useQueryApi.ts (1)
  • useApiQuery (29-49)
cypress/e2e/index.cy.ts (2)
src/web/urlConfig.ts (2)
  • createTestNode (82-93)
  • SIGNED_IN (39-45)
cypress/common/tools.ts (1)
  • visitWithState (5-9)
src/ui/client/useQueryApi.ts (2)
src/ui/client/apiSchema.generated.ts (1)
  • paths (7-895)
src/ui/client/apiSchema.custom.ts (1)
  • customPaths (6-14)
src/ui/views/Settings/StringsSection.tsx (1)
src/ui/components/InfoTooltip/InfoTooltip.tsx (1)
  • InfoTooltip (15-33)
src/ui/views/Index/ListItem.tsx (1)
src/ui/components/NamespaceSelect/NamespaceSelect.tsx (1)
  • NamespaceSelect (15-96)
src/ui/views/Settings/PushSection.tsx (1)
src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx (1)
  • AutocompleteSelect (39-327)
src/ui/client/apiSchema.custom.ts (1)
src/ui/client/apiSchema.generated.ts (2)
  • operations (3321-11264)
  • components (899-3315)
src/ui/views/Push/Push.tsx (5)
src/tools/getPushChanges.ts (2)
  • KeyChanges (17-22)
  • getPushChanges (24-105)
src/types.ts (1)
  • NodeInfo (74-86)
src/ui/hooks/useAllTranslations.ts (1)
  • useAllTranslations (12-108)
src/ui/hooks/useHasNamespacesEnabled.ts (1)
  • useHasNamespacesEnabled (3-35)
src/main/endpoints/getScreenshots.ts (1)
  • getScreenshotsEndpoint (9-54)
src/ui/views/Index/Index.tsx (2)
src/ui/hooks/useHasNamespacesEnabled.ts (1)
  • useHasNamespacesEnabled (3-35)
src/ui/hooks/useConnectedNodes.ts (1)
  • useConnectedNodes (8-16)
src/tools/getPushChanges.ts (1)
src/tools/compareNs.ts (1)
  • compareNs (1-3)
src/ui/views/Settings/ProjectSettings.tsx (3)
src/ui/hooks/useHasNamespacesEnabled.ts (1)
  • useHasNamespacesEnabled (3-35)
src/ui/components/InfoTooltip/InfoTooltip.tsx (1)
  • InfoTooltip (15-33)
src/ui/client/decodeApiKey.ts (1)
  • getProjectIdFromApiKey (57-74)
src/ui/client/client.ts (2)
src/ui/client/apiSchema.generated.ts (1)
  • paths (7-895)
src/ui/client/apiSchema.custom.ts (1)
  • customPaths (6-14)
🪛 GitHub Check: Static check 🪲
src/ui/views/Settings/Settings.tsx

[warning] 23-23:
'getProjectIdFromApiKey' is defined but never used


[warning] 15-15:
'useApiQuery' is defined but never used

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cypress
🔇 Additional comments (52)
src/ui/views/Pull/Pull.tsx (1)

56-59: LGTM! Clear error handling for image upload limits.

The error handling logic is correct and the error message provides clear, actionable guidance to users.

src/ui/styles.css (1)

39-39: LGTM!

The addition of the dark mode text color variable is appropriate. White text (#ffffff) provides good contrast on dark backgrounds.

src/ui/views/Settings/StringsEditor.css (1)

162-166: LGTM!

The dark mode override for placeholder text improves readability. Using --figma-color-text-primary (white) instead of grey text provides better contrast on dark backgrounds.

cypress/e2e/index.cy.ts (1)

25-29: LGTM! Selector and assertion correctly updated.

The migration from a select dropdown to an input-based namespace selector is properly reflected here. The functional expectation checking for an empty value is appropriate.

src/ui/components/ActionsBottom/ActionsBottom.css (1)

10-10: LGTM! Z-index adjustment aligns with UI layering updates.

The z-index increase from 1 to 2 ensures proper stacking with other overlay components introduced in this PR.

src/ui/components/InfoTooltip/InfoTooltip.css (1)

6-6: LGTM! Z-index addition ensures proper tooltip layering.

The added z-index coordinates with other UI stacking adjustments across the PR.

src/ui/hooks/useSetNodesDataMutation.ts (1)

9-20: LGTM! Query invalidation pattern ensures data freshness.

The onSuccess handler properly invalidates the connected nodes query after mutation succeeds, ensuring the UI displays up-to-date data.

src/ui/components/NodeList/NodeRow.tsx (1)

88-88: LGTM! Improved label clarity.

Changing "ns:" to "Namespace:" makes the UI more user-friendly and explicit.

src/types.ts (1)

118-118: LGTM! Settings type extension for hidden layer handling.

The new ignoreHiddenLayersIncludingChildren property appropriately extends the GlobalSettings type for parent-chain visibility checks.

src/ui/components/NodeList/NodeRow.css (1)

41-43: LGTM! CSS changes support improved namespace display.

The overflow and text-overflow adjustments allow the full namespace text to be visible, complementing the "Namespace:" label change.

package.json (1)

9-9: LGTM! Schema generation script updated for new API structure.

The updated endpoint URL and output path align with the UI-centric API schema organization introduced in this PR.

src/ui/hooks/useAllTranslations.ts (1)

101-103: LGTM!

The clearCache() method provides a clean way to reset translations data, which aligns with the broader namespace refresh functionality introduced in this PR.

src/ui/components/NamespaceSelect/NamespaceSelect.css (1)

11-63: LGTM!

The styling for the NamespaceSelect component is well-structured with:

  • Proper positioning and layout for the input wrapper and refresh button
  • Clear interaction states (hover, disabled)
  • Smooth rotation animation for the refresh action
src/main/utils/settingsTools.ts (1)

76-76: LGTM!

The new ignoreHiddenLayersIncludingChildren setting is properly integrated into the plugin data flow, being destructured from input and persisted to document settings. This aligns with the PR's objective to refine hidden layer detection logic.

Also applies to: 95-95

src/ui/icons/SvgIcons.tsx (1)

187-201: LGTM!

The new Refresh icon component follows the established pattern of other icon components in this file, using currentColor for flexible theming and spreading props for maximum reusability.

src/ui/views/Index/Index.tsx (3)

67-87: LGTM!

The allAvailableNamespaces computation efficiently combines API and node-based namespaces with proper deduplication and sorting. The sorting logic correctly places empty string at the end while maintaining alphabetical order for other namespaces.


90-92: LGTM!

The handleRefreshNamespaces function properly refreshes both the API namespaces and connected nodes data sources using Promise.all, ensuring the UI stays in sync with the latest namespace state.


241-246: LGTM!

The updated ListItem props correctly wire up the namespace enablement flag, dynamically computed namespace list, and refresh callback, enabling the component to conditionally render namespace selection UI and trigger refreshes when needed.

cypress/e2e/push.cy.ts (2)

82-115: LGTM!

The test properly verifies that pushing new keys displays the correct count in the success message. The test flow is clear: setup test data → verify diff display → submit push → assert success count.


117-150: LGTM!

The test properly verifies that pushing changed keys displays the correct count in the success message, following the same reliable pattern as the new keys test.

src/ui/components/AutocompleteSelect/AutocompleteSelect.css (1)

1-61: LGTM!

The AutocompleteSelect styling is well-structured with:

  • Proper positioning variants for dropdown direction (up/down)
  • Clear visual hierarchy with headers and items
  • Appropriate interaction states (hover, cursor)
  • Themed colors using Figma CSS variables
src/tools/getPushChanges.ts (3)

38-50: LGTM on conditional namespace comparison.

The logic correctly skips namespace comparison when hasNamespacesEnabled is false, using short-circuit evaluation with compareNs.


85-93: LGTM on KeyChangeValue construction.

Setting ns to undefined when namespaces are disabled ensures consistent downstream handling.


24-30: LGTM on the function signature change.

The parameter change to hasNamespacesEnabled correctly controls namespace behavior in the push logic. The implementation properly guards namespace comparisons (line 42) and conditionally sets the ns field (line 87). All callers have been updated—the single caller in Push.tsx correctly passes the new parameter.

src/main/utils/nodeTools.ts (1)

34-59: LGTM on the hidden layer visibility logic.

The refactored logic correctly:

  1. Checks node's own visibility first
  2. Optionally traverses the parent chain when ignoreHiddenLayersIncludingChildren is enabled
  3. Uses defensive programming with try-catch around parent traversal

One consideration: the catch block logs the error but allows the function to continue (potentially returning true). This is a reasonable fallback—if we can't determine parent visibility, we include the node rather than silently exclude it.

src/ui/client/client.ts (1)

124-127: LGTM on the extended Paths type.

The intersection paths & customPaths correctly extends the API client to support both generated and custom endpoint definitions.

src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts (1)

1-12: LGTM!

Standard CSS module type declaration file with correct readonly properties and CommonJS-style export.

src/ui/components/NamespaceSelect/NamespaceSelect.tsx (1)

65-95: LGTM on the component structure.

The integration with AutocompleteSelect and conditional refresh button rendering is clean. The isRefreshing state correctly disables the button and applies the rotating animation.

src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts (1)

1-12: LGTM!

The CSS type definitions correctly expose the new class names needed for the enhanced NamespaceSelect UI (container, refresh button, animations, etc.).

src/ui/views/Settings/StringsSection.tsx (3)

82-98: LGTM!

The help text clearly explains the behavior of the "Including all child texts" option and how it differs from the basic hidden layers setting.


139-142: LGTM!

State management for ignoreHiddenLayersIncludingChildren follows the existing patterns consistently, with proper initialization from config and handler to update both local state and config.

Also applies to: 203-211


308-329: LGTM!

The UI properly implements the nested checkbox pattern with appropriate indentation and conditional rendering. The InfoTooltip provides helpful context for users.

src/ui/views/Index/ListItem.tsx (3)

17-29: LGTM!

The props interface correctly extends to support namespace enablement detection and refresh behavior. The function signature properly destructures the new props.


101-111: LGTM!

The NamespaceSelect rendering is now properly gated by hasNamespacesEnabled and the onRefresh callback is correctly passed through for refresh functionality.


125-136: LGTM!

The simplified InsertLink rendering based on node.connected is cleaner and maintains the same visual behavior (secondary color for unconnected state).

src/ui/views/Settings/ProjectSettings.tsx (2)

136-142: LGTM!

The namespace sorting logic correctly places empty strings at the end while maintaining alphabetical order for named namespaces.


20-77: LGTM!

The dynamic help text functions properly generate project-specific documentation links. The dual functions (namespaceHelpText and namespaceHelpTextSetUp) provide appropriate guidance for both enabled and disabled namespace states.

src/ui/client/apiSchema.custom.ts (1)

1-68: LGTM!

The custom API schema is well-structured and follows the same patterns as the generated schema. It properly:

  • References existing components["schemas"] types for consistency
  • Includes comprehensive response types for success and error cases
  • Provides clear documentation comments
  • Types the projectId parameter correctly as a number
src/ui/views/Settings/PushSection.tsx (2)

25-30: LGTM!

The tag options are efficiently computed with useMemo and properly sorted alphabetically for a better user experience.


88-97: LGTM!

The AutocompleteSelect integration simplifies the tag management UI significantly. The configuration is appropriate with multi-select mode enabled and descriptive placeholders for existing and new tags.

src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx (5)

13-37: LGTM!

The discriminated union for Props correctly types single-select vs multi-select modes, ensuring type safety for value and onChange at compile time.


73-99: LGTM!

The display logic and sync effect correctly handle the single-select mode behavior:

  • Shows the selected value when not focused
  • Shows the input value when focused
  • Syncs back to the selected value when focus is lost

101-129: LGTM!

The filtering and validation logic correctly:

  • Filters options based on input
  • Excludes selected values in multi-select mode
  • Detects exact matches
  • Prevents duplicate additions

130-166: LGTM!

Event handlers properly manage both single-select and multi-select flows with appropriate type guards and state updates.


193-326: LGTM!

The rendering logic is comprehensive:

  • Displays badges for multi-select values with remove functionality
  • Provides accessible input with proper ARIA attributes
  • Renders dropdown with dynamic positioning
  • Highlights matching text segments
  • Shows "Add" option for new values
  • Handles empty states appropriately
src/ui/views/Push/Push.tsx (7)

44-50: LGTM!

The new state variables (isPushing, pushedKeysCount, screenshotCount) appropriately support the enhanced push progress tracking and success screen functionality.


56-68: Good performance optimization.

The O(n²) to O(n) deduplication improvement using a Set is a solid change. The composite key correctly handles both key and namespace for uniqueness.


111-145: Well-structured effect with proper cancellation handling.

The cancellation pattern using a cancelled flag correctly prevents state updates after unmount or dependency changes. The early return when success is true is a good optimization. The use of primitive keys as dependencies to avoid object reference issues is a valid pattern.


352-379: Good resilience pattern for non-critical operations.

Wrapping tag addition in a separate try-catch allows the push to succeed even if tagging fails. The error message correctly indicates that translations were still updated.


412-419: Cache invalidation correctly preserves pushed count.

Storing keysPushed before clearing cache ensures the success screen displays accurate counts. The cache clear prevents stale translation data on subsequent operations.


474-488: Clean success screen implementation.

The success UI correctly uses preserved counts and provides clear feedback to the user with the option to return to the main view.


489-490: LGTM!

Including isPushing in the loading condition correctly shows the loading state during the push operation.

Comment on lines +25 to +37
const allNamespaces = [
...new Set([
...namespaces,
...(value && !namespaces.includes(value) ? [value] : []),
]),
]
.filter(Boolean)
.sort((a, b) => {
// Sort alphabetically, but put empty string at the end
if (!a) return 1;
if (!b) return -1;
return a.localeCompare(b);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: .filter(Boolean) removes empty strings that the sort logic expects to handle.

Line 31 filters out all falsy values including empty strings (""), but the sort comparator on lines 33-36 explicitly handles empty strings to "put empty string at the end." Since empty strings are filtered out before sorting, this code path is unreachable.

If empty string represents a valid "no namespace" option, preserve it:

   ]
-    .filter(Boolean)
+    .filter((ns) => ns !== undefined && ns !== null)
     .sort((a, b) => {

Alternatively, if empty strings should be excluded, remove the dead code in the sort comparator.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const allNamespaces = [
...new Set([
...namespaces,
...(value && !namespaces.includes(value) ? [value] : []),
]),
]
.filter(Boolean)
.sort((a, b) => {
// Sort alphabetically, but put empty string at the end
if (!a) return 1;
if (!b) return -1;
return a.localeCompare(b);
});
const allNamespaces = [
...new Set([
...namespaces,
...(value && !namespaces.includes(value) ? [value] : []),
]),
]
.filter((ns) => ns !== undefined && ns !== null)
.sort((a, b) => {
// Sort alphabetically, but put empty string at the end
if (!a) return 1;
if (!b) return -1;
return a.localeCompare(b);
});
🤖 Prompt for AI Agents
In src/ui/components/NamespaceSelect/NamespaceSelect.tsx around lines 25 to 37,
the current .filter(Boolean) removes empty strings but the sort comparator
expects to handle empty string specially; replace .filter(Boolean) with a filter
that only removes null/undefined (e.g., .filter(x => x !== undefined && x !==
null)) so empty string values are preserved for sorting and the "put empty
string at the end" logic remains reachable.

Comment on lines 3 to 35
export const useHasNamespacesEnabled = () => {
try {
// First, get the project ID from the API key info (relevant for legacy api keys)
const apiKeyInfo = useApiQuery({
url: "/v2/api-keys/current",
method: "get",
options: {
cacheTime: 60000,
staleTime: 60000,
},
});

// Then get the project info if we have a project ID
// eslint-disable-next-line react-hooks/rules-of-hooks
const projectQuery = useApiQuery({
url: "/v2/projects/{projectId}",
method: "get",
path: {
projectId: apiKeyInfo.data?.projectId ?? 0,
},
options: {
cacheTime: 60000,
staleTime: 60000,
enabled: apiKeyInfo.data?.projectId !== undefined,
},
});

return projectQuery.data?.useNamespaces ?? false;
} catch (error) {
console.error(error);
return false;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove try/catch wrapper around hook calls.

Wrapping useApiQuery calls in try/catch violates React's Rules of Hooks. Hooks must be called unconditionally at the top level of a component or custom hook. The try/catch won't catch rendering errors from hooks, and the eslint-disable comment on line 16 suggests awareness of this issue.

The conditional enabling of projectQuery via options.enabled (line 26) is the correct approach for conditional data fetching.

Apply this diff to fix the issue:

 export const useHasNamespacesEnabled = () => {
-  try {
-    // First, get the project ID from the API key info (relevant for legacy api keys)
-    const apiKeyInfo = useApiQuery({
-      url: "/v2/api-keys/current",
-      method: "get",
-      options: {
-        cacheTime: 60000,
-        staleTime: 60000,
-      },
-    });
+  // First, get the project ID from the API key info (relevant for legacy api keys)
+  const apiKeyInfo = useApiQuery({
+    url: "/v2/api-keys/current",
+    method: "get",
+    options: {
+      cacheTime: 60000,
+      staleTime: 60000,
+    },
+  });
 
-    // Then get the project info if we have a project ID
-    // eslint-disable-next-line react-hooks/rules-of-hooks
-    const projectQuery = useApiQuery({
-      url: "/v2/projects/{projectId}",
-      method: "get",
-      path: {
-        projectId: apiKeyInfo.data?.projectId ?? 0,
-      },
-      options: {
-        cacheTime: 60000,
-        staleTime: 60000,
-        enabled: apiKeyInfo.data?.projectId !== undefined,
-      },
-    });
+  // Then get the project info if we have a project ID
+  const projectQuery = useApiQuery({
+    url: "/v2/projects/{projectId}",
+    method: "get",
+    path: {
+      projectId: apiKeyInfo.data?.projectId ?? 0,
+    },
+    options: {
+      cacheTime: 60000,
+      staleTime: 60000,
+      enabled: apiKeyInfo.data?.projectId !== undefined,
+    },
+  });
 
-    return projectQuery.data?.useNamespaces ?? false;
-  } catch (error) {
-    console.error(error);
-    return false;
-  }
+  return projectQuery.data?.useNamespaces ?? false;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useHasNamespacesEnabled = () => {
try {
// First, get the project ID from the API key info (relevant for legacy api keys)
const apiKeyInfo = useApiQuery({
url: "/v2/api-keys/current",
method: "get",
options: {
cacheTime: 60000,
staleTime: 60000,
},
});
// Then get the project info if we have a project ID
// eslint-disable-next-line react-hooks/rules-of-hooks
const projectQuery = useApiQuery({
url: "/v2/projects/{projectId}",
method: "get",
path: {
projectId: apiKeyInfo.data?.projectId ?? 0,
},
options: {
cacheTime: 60000,
staleTime: 60000,
enabled: apiKeyInfo.data?.projectId !== undefined,
},
});
return projectQuery.data?.useNamespaces ?? false;
} catch (error) {
console.error(error);
return false;
}
};
export const useHasNamespacesEnabled = () => {
// First, get the project ID from the API key info (relevant for legacy api keys)
const apiKeyInfo = useApiQuery({
url: "/v2/api-keys/current",
method: "get",
options: {
cacheTime: 60000,
staleTime: 60000,
},
});
// Then get the project info if we have a project ID
const projectQuery = useApiQuery({
url: "/v2/projects/{projectId}",
method: "get",
path: {
projectId: apiKeyInfo.data?.projectId ?? 0,
},
options: {
cacheTime: 60000,
staleTime: 60000,
enabled: apiKeyInfo.data?.projectId !== undefined,
},
});
return projectQuery.data?.useNamespaces ?? false;
};
🤖 Prompt for AI Agents
In src/ui/hooks/useHasNamespacesEnabled.ts around lines 3 to 35, remove the
outer try/catch and the eslint-disable comment so hooks are called
unconditionally at the top level; call useApiQuery for apiKeyInfo and
projectQuery directly (no try/catch), pass projectId as
apiKeyInfo.data?.projectId ?? undefined and keep options.enabled:
apiKeyInfo.data?.projectId !== undefined so projectQuery is conditionally
active, then return projectQuery.data?.useNamespaces ?? false.

Comment on lines +193 to +207
{hasNamespacesEnabled ? (
<InfoTooltip>
{namespaceHelpText({
apiUrl,
projectId: getProjectIdFromApiKey(apiKey) ?? 0,
})}
</InfoTooltip>
) : (
<InfoTooltip>
{namespaceHelpTextSetUp({
apiUrl,
projectId: getProjectIdFromApiKey(apiKey) ?? 0,
})}
</InfoTooltip>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for getProjectIdFromApiKey function definition
rg "getProjectIdFromApiKey" --type ts --type tsx -B 2 -A 10

Repository: tolgee/figma-plugin

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Search for getProjectIdFromApiKey function definition in the repository
rg "getProjectIdFromApiKey" -B 2 -A 10

Repository: tolgee/figma-plugin

Length of output: 5358


🏁 Script executed:

#!/bin/bash
# Get the complete implementation of the getProjectIdFromApiKey function
cat src/ui/client/decodeApiKey.ts

Repository: tolgee/figma-plugin

Length of output: 1890


🏁 Script executed:

#!/bin/bash
# Search for namespaceHelpText and namespaceHelpTextSetUp function definitions
rg "namespaceHelpText" -B 2 -A 15

Repository: tolgee/figma-plugin

Length of output: 4073


🏁 Script executed:

#!/bin/bash
# Search for how apiKey is validated before being passed to these functions
rg "apiKey" src/ui/views/Settings/ProjectSettings.tsx -B 2 -A 2 | head -50

Repository: tolgee/figma-plugin

Length of output: 738


Add validation before constructing help text URLs when API key cannot be parsed.

The code uses getProjectIdFromApiKey(apiKey) ?? 0 as a fallback, which produces broken links like /projects/0/manage/edit/advanced when the API key is empty, malformed, or lacks the required tgpak_ prefix. Consider either conditionally rendering the help text only when a valid project ID is available, or validating the API key earlier in the component lifecycle.

🤖 Prompt for AI Agents
In src/ui/views/Settings/ProjectSettings.tsx around lines 193 to 207, the
component forces a fallback projectId of 0 when getProjectIdFromApiKey(apiKey)
fails which produces invalid help links; change the rendering to validate the
parsed projectId first and only pass a valid numeric projectId into
namespaceHelpText/namespaceHelpTextSetUp (or omit/render a different tooltip)
when getProjectIdFromApiKey(apiKey) returns a truthy number — do not use ?? 0;
if the parsed id is falsy, render a generic help message or hide the link so
broken URLs like /projects/0/... are never generated.

Copy link
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: 1

🧹 Nitpick comments (1)
src/ui/views/Push/Push.tsx (1)

82-92: Consider extracting namespace computation to avoid duplication.

This namespace extraction logic is duplicated in the useEffect (lines 125-129). While both implementations are correct, extracting it to a memoized value would improve maintainability.

Consider refactoring:

-  const requiredNamespacesKey = useMemo(() => {
-    if (!hasNamespacesEnabled) return "";
-    const uniqueNamespaces = Array.from(
-      new Set(
-        deduplicatedNodes
-          .map((node) => node.ns)
-          .filter((ns): ns is string => Boolean(ns))
-      )
-    );
-    return uniqueNamespaces.sort().join(",");
-  }, [deduplicatedNodes, hasNamespacesEnabled]);
+  const requiredNamespaces = useMemo(() => {
+    if (!hasNamespacesEnabled || deduplicatedNodes.length === 0) return undefined;
+    return Array.from(
+      new Set(deduplicatedNodes.map((node) => node.ns ?? ""))
+    );
+  }, [deduplicatedNodes, hasNamespacesEnabled]);
+
+  const requiredNamespacesKey = useMemo(() => {
+    return requiredNamespaces ? requiredNamespaces.slice().sort().join(",") : "";
+  }, [requiredNamespaces]);

Then use requiredNamespaces directly in the useEffect instead of recomputing it at lines 124-129.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3f8c04 and b95adad.

📒 Files selected for processing (1)
  • src/ui/views/Push/Push.tsx (10 hunks)
🔇 Additional comments (6)
src/ui/views/Push/Push.tsx (6)

56-68: LGTM! Efficient deduplication optimization.

The Set-based approach correctly reduces complexity from O(n²) to O(n), and the composite key properly accounts for namespace differentiation.


111-193: LGTM! Well-implemented diff computation with cancellation.

The centralized useEffect correctly handles async operations with proper cancellation to prevent stale state updates. The comprehensive dependency array ensures recomputation when relevant inputs change, and the early return when success is true avoids unnecessary work.


398-398: LGTM! Stale closure issue resolved.

The functional updater setUploadedScreenshotCount((c) => c + 1) correctly addresses the stale closure bug mentioned in past review comments. This ensures the count increments properly across async iterations.


413-422: LGTM! Proper state management and cache invalidation.

Storing the pushed keys count before clearing the cache and connecting nodes ensures accurate UI display, and clearing the translations cache ensures newly pushed keys are recognized on subsequent operations.


475-489: LGTM! Clear success feedback with accurate counts.

The success screen provides immediate feedback using the stored pushedKeysCount and uploadedScreenshotCount, avoiding the need to wait for diff recomputation. The conditional display based on uploadScreenshots appropriately reflects the user's choice.


266-268: The ArrayBuffer cast is correct and safe.

screenshot.image is typed as Uint8Array (not Node.js Buffer), and accessing .buffer on a Uint8Array returns the underlying ArrayBuffer. The cast is redundant but harmless—the Blob constructor accepts both ArrayBuffer and typed arrays like Uint8Array directly.

Likely an incorrect or invalid review comment.

Copy link
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: 0

♻️ Duplicate comments (1)
src/ui/views/Push/Push.tsx (1)

443-448: Reset uploadedScreenshotCount in handleRepeat.

Similar to the past review comment about screenshot count, if a push partially completes (some screenshots uploaded) before failing, uploadedScreenshotCount retains its value. On retry, the count will be incorrect since it starts from the previous partial value instead of zero.

Apply this diff to reset the screenshot count:

 const handleRepeat = () => {
   setError(false);
   setSuccess(false);
   setPushedKeysCount(0);
+  setUploadedScreenshotCount(0);
   setErrorMessage(undefined);
 };
🧹 Nitpick comments (1)
src/ui/views/Push/Push.tsx (1)

320-332: Optional: Simplify resolution for changedKeys.

Since changedKeys always have oldValue (by definition from getPushChanges), the conditional on line 328 will always evaluate to "OVERRIDE". Consider simplifying:

         translations: {
           [language]: {
             text: item.newValue,
-            resolution: item.oldValue ? "OVERRIDE" : "NEW",
+            resolution: "OVERRIDE",
           },
         },

This is defensive programming and not a bug, but simplification improves clarity.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b95adad and 30152a1.

📒 Files selected for processing (3)
  • cypress/e2e/index.cy.ts (1 hunks)
  • cypress/e2e/push.cy.ts (2 hunks)
  • src/ui/views/Push/Push.tsx (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/views/Push/Push.tsx (5)
src/tools/getPushChanges.ts (2)
  • KeyChanges (17-22)
  • getPushChanges (24-105)
src/ui/hooks/useConnectedNodes.ts (1)
  • useConnectedNodes (8-16)
src/types.ts (1)
  • NodeInfo (74-86)
src/ui/hooks/useHasNamespacesEnabled.ts (1)
  • useHasNamespacesEnabled (3-35)
src/main/endpoints/getScreenshots.ts (1)
  • getScreenshotsEndpoint (9-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cypress
🔇 Additional comments (18)
cypress/e2e/push.cy.ts (3)

82-115: LGTM! Well-structured test for new keys.

The test follows existing patterns, includes clear comments, and appropriately verifies that the success message displays the correct count of pushed keys.


117-150: LGTM! Well-structured test for changed keys.

The test follows existing patterns, includes clear comments, and appropriately verifies that the success message displays the correct count of pushed keys.


154-154: Verify test independence.

The text change from "On the road" to "Changed text 2" aligns with the new test at line 120 that uses the same key. However, Cypress tests should be isolated and independent. Please confirm that:

  1. Tests don't share state or depend on execution order
  2. This change is for clarity/consistency rather than to fix test interdependence

If these tests are truly independent (as they should be), the original text "On the road" should work just as well as "Changed text 2".

Also applies to: 164-164

cypress/e2e/index.cy.ts (2)

25-29: LGTM! Selector and assertion updated correctly for AutocompleteSelect.

The change from a select dropdown to general_namespace_select_input and the functional assertion with the || "" fallback appropriately handle the new AutocompleteSelect component behavior, where the value might be null or undefined.


54-54: The selectors in lines 54 and 84-87 are correct and should not be changed. general_node_list_row_namespace targets a read-only display element in the NodeRow component for connected nodes, while general_namespace_select_input targets an editable AutocompleteSelect input in the NamespaceSelect component for unconnected nodes. These serve different UI purposes and intentionally use different selectors.

src/ui/views/Push/Push.tsx (13)

32-32: LGTM! Namespace detection and progress tracking state additions.

The new state variables (isPushing, pushedKeysCount, uploadedScreenshotCount) and the useHasNamespacesEnabled hook properly support the namespace-aware push flow and progress tracking requirements.

Also applies to: 44-44, 47-47, 50-50, 71-71


56-68: Excellent performance optimization!

The deduplication logic correctly reduces time complexity from O(n²) to O(n) using a Set, and properly includes namespace in the composite key to avoid conflating identically named keys from different namespaces.


73-110: Well-designed memoization strategy.

The memoized keys (nodesKey, requiredNamespacesKey, screenshotsKey) and extracted primitives from tolgeeConfig effectively stabilize the useEffect dependencies, preventing unnecessary recomputations while capturing all relevant changes including namespace updates.


111-193: Robust async effect with proper cancellation.

The refactored diff computation properly handles:

  • Namespace-aware translation loading
  • Cancellation to prevent race conditions
  • Comprehensive error handling with specific messages for invalid API keys
  • Stable dependencies via memoized keys

The effect safely uses deduplicatedNodes without including it in the deps array because changes to nodes are captured by nodesKey.


195-197: LGTM! Appropriate memoization.

The totalScreenshotCount is correctly memoized from the changes state and used consistently throughout the UI.


255-256: LGTM! Proper counter initialization.

The counters are correctly reset before starting the push operation.


266-268: LGTM! Correct ArrayBuffer cast.

The cast of screenshot.image.buffer as ArrayBuffer is correct; Uint8Array.buffer returns an ArrayBuffer, which is the proper type for Blob construction.


353-411: Excellent error handling with partial success support.

The granular error handling allows the push to succeed even if tag application or screenshot updates fail, with clear user-facing messages. Line 398 correctly uses a functional state updater setUploadedScreenshotCount((c) => c + 1) to avoid stale closure bugs in the async loop.


413-420: LGTM! Proper cache management.

The key count is correctly stored before clearing the translation cache, ensuring the success screen displays accurate counts while the cache is refreshed for subsequent operations.


423-436: LGTM! Comprehensive error handling.

The enhanced error messages provide specific, actionable feedback for common failure scenarios including invalid API keys, too many images, and import errors.


456-460: LGTM! Correct changesSize computation.

Using pushedKeysCount when displaying the success state ensures the UI shows the actual number of keys that were pushed, regardless of any subsequent diff recomputations.


475-489: LGTM! Improved success screen UX.

The immediate success screen provides clear feedback with accurate counts, and the success state in the useEffect dependencies (line 192) correctly prevents unnecessary recomputation while showing the success message.


490-490: LGTM! Consistent UI updates.

The UI consistently uses totalScreenshotCount and properly includes isPushing in the loading condition, ensuring accurate progress display throughout the push flow.

Also applies to: 507-520, 531-532

@eweren
Copy link
Contributor Author

eweren commented Jan 2, 2026

@JanCizmar please merge this, if it's fine :)

Copy link
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: 1

🧹 Nitpick comments (2)
src/ui/views/Push/Push.tsx (2)

147-152: Consider simplifying config reconstruction.

The configForDiff object spreads tolgeeConfig and then re-assigns the same fields that are already present. You can pass tolgeeConfig directly to getPushChanges or only override fields that differ from the source.

🔎 Simplified approach
-        // Reconstruct tolgeeConfig object for getPushChanges
-        const configForDiff = {
-          ...tolgeeConfig,
-          tags: tolgeeConfig?.tags,
-          updateScreenshots: tolgeeConfigUpdateScreenshots,
-          addTags: tolgeeConfigAddTags,
-        };
-
         setChanges(
           getPushChanges(
             deduplicatedNodes,
             translations,
             hasNamespacesEnabled,
             screenshots,
-            configForDiff
+            tolgeeConfig
           )
         );

382-411: Functional state updater correctly prevents stale closure.

Line 398 correctly uses setUploadedScreenshotCount((c) => c + 1) to avoid the stale closure bug flagged in previous reviews. The individual try-catch blocks enable partial success when updating screenshots.

Minor improvement: If multiple screenshots fail, only the last error message is preserved. Consider accumulating errors or showing a count of failures for better UX.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30152a1 and 39a33d2.

📒 Files selected for processing (3)
  • src/ui/client/client.ts
  • src/ui/hooks/useHasNamespacesEnabled.ts
  • src/ui/views/Push/Push.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/hooks/useHasNamespacesEnabled.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cypress
🔇 Additional comments (18)
src/ui/client/client.ts (3)

6-6: LGTM: Custom paths import added.

The import of customPaths is used appropriately to extend the API path definitions.


140-146: Critical issue from past review has been fixed!

The unreachable condition issue has been correctly resolved. The code now checks if pathParams.projectId is undefined (line 142) before assigning to it (line 145), making the conditional logic for URL rewriting work as intended.

The current flow is correct:

  1. Extract projectId from API key
  2. If projectId exists and wasn't already provided in pathParams, add the placeholder to the URL
  3. Assign projectId to pathParams for placeholder replacement

Note: Line 145 will overwrite any explicit pathParams.projectId with the value from the API key, which appears intentional as the API key is the source of truth.


127-127: No type conflicts found.

The intersection type paths & customPaths is safe. The customPaths interface adds the single endpoint "/v2/projects/{projectId}" which does not exist in the generated paths, so there are no overlapping keys and no type conflicts.

src/ui/views/Push/Push.tsx (15)

56-68: LGTM! Excellent deduplication optimization.

The refactoring from O(n²) to O(n) using a Set is a solid performance improvement. The composite key properly accounts for both key and namespace.


71-71: Good addition for namespace-aware logic.

The useHasNamespacesEnabled hook integrates well with the namespace-aware refactoring mentioned in the PR objectives.


74-109: Excellent memoization strategy.

The stable keys (nodesKey, requiredNamespacesKey, screenshotsKey) and extracted primitive values from tolgeeConfig are well-designed optimizations that prevent unnecessary recomputation while maintaining correctness.


195-197: LGTM!

The memoized totalScreenshotCount is a good optimization for UI consistency.


255-257: LGTM!

Proper initialization of push state counters before starting the push operation.


328-328: LGTM!

The conditional resolution logic correctly distinguishes between overriding existing translations and creating new ones based on the presence of oldValue.


353-380: Good partial-success error handling.

The try-catch around tagging allows the push to succeed even if tagging fails, with a clear message to the user. This graceful degradation improves UX when non-critical operations fail.


414-420: LGTM! Proper cache invalidation.

Storing the pushed key count before clearing the cache and then calling clearCache() ensures newly pushed keys will be recognized in subsequent operations.


427-435: LGTM! Enhanced error handling.

The specific error cases for too_many_uploaded_images and import_keys_error provide better user feedback than generic error messages.


443-449: LGTM! Past review feedback addressed.

The reset logic now correctly includes both setPushedKeysCount(0) and setUploadedScreenshotCount(0), addressing the issues flagged in previous reviews about incomplete state resets on retry.


476-490: LGTM! Improved success screen UX.

The success screen displays immediately after push completion with clear metrics (pushed keys count and uploaded screenshots count), providing good user feedback without waiting for data recomputation.


491-491: LGTM!

Adding isPushing to the loading condition correctly prevents user interaction during the push operation.


508-521: LGTM!

Consistent use of totalScreenshotCount throughout the UI improves code clarity and maintainability.


532-544: LGTM!

The finish button logic correctly handles the case where there are no changes and either no screenshots or screenshots are not being uploaded.


266-268: The type system already guarantees that screenshot.image.buffer is an ArrayBuffer since screenshot.image is typed as Uint8Array. The cast is redundant but does not cause issues.

Comment on lines +111 to +193
useEffect(() => {
// Don't recompute if we're showing success screen
if (success) {
return;
}

let cancelled = false;

async function computeDiff() {
setLoadingStatus("Loading translations");
try {
// Compute requiredNamespaces inside the effect to avoid dependency issues
// Include all namespaces from nodes, including empty string for default namespace
const requiredNamespaces =
hasNamespacesEnabled && deduplicatedNodes.length > 0
? Array.from(
new Set(deduplicatedNodes.map((node) => node.ns ?? ""))
)
: undefined;

const translations = await allTranslationsLoadable.getData({
language,
namespaces: requiredNamespaces,
});

// Check if cancelled before expensive screenshot operation
if (cancelled) return;

const screenshots = tolgeeConfigUpdateScreenshots
? await getScreenshotsEndpoint.call(nodes)
: [];

setChanges(
getPushChanges(
deduplicatedNodes,
translations,
language,
screenshots,
tolgeeConfig
)
);
} catch (e) {
if (e === "invalid_project_api_key") {
setErrorMessage("Invalid API key");
} else {
setErrorMessage(`Cannot get translation data. ${e}`);
// Check if cancelled before setting state
if (cancelled) return;

// Reconstruct tolgeeConfig object for getPushChanges
const configForDiff = {
...tolgeeConfig,
tags: tolgeeConfig?.tags,
updateScreenshots: tolgeeConfigUpdateScreenshots,
addTags: tolgeeConfigAddTags,
};

setChanges(
getPushChanges(
deduplicatedNodes,
translations,
hasNamespacesEnabled,
screenshots,
configForDiff
)
);
} catch (e) {
if (cancelled) return;
if (e === "invalid_project_api_key") {
setErrorMessage("Invalid API key");
} else {
setErrorMessage(`Cannot get translation data. ${e}`);
}
} finally {
if (!cancelled) {
setLoadingStatus(undefined);
}
}
} finally {
setLoadingStatus(undefined);
}
}

useEffect(() => {
computeDiff();
}, [nodes.length]);

// Cleanup: cancel if dependencies change before async operations complete
return () => {
cancelled = true;
};
}, [
nodesKey,
screenshotsKey,
hasNamespacesEnabled,
language,
requiredNamespacesKey,
tolgeeConfigTags,
tolgeeConfigUpdateScreenshots,
tolgeeConfigAddTags,
success, // Include success to prevent recompute when showing success screen
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the useAllTranslations hook definition
rg "export.*useAllTranslations|const useAllTranslations" --type tsx --type ts -A 10

Repository: tolgee/figma-plugin

Length of output: 90


🏁 Script executed:

# Search for deduplicatedNodes definition in Push.tsx
rg "deduplicatedNodes" src/ui/views/Push/Push.tsx -B 3 -A 1

Repository: tolgee/figma-plugin

Length of output: 1315


🏁 Script executed:

# Check imports at the top of Push.tsx to understand allTranslationsLoadable
head -50 src/ui/views/Push/Push.tsx

Repository: tolgee/figma-plugin

Length of output: 2260


🏁 Script executed:

# Find where allTranslationsLoadable is defined/imported
rg "allTranslationsLoadable" src/ui/views/Push/Push.tsx -B 2 -A 2

Repository: tolgee/figma-plugin

Length of output: 805


🏁 Script executed:

# Find useAllTranslations hook definition
rg "useAllTranslations" --type ts -l

Repository: tolgee/figma-plugin

Length of output: 289


🏁 Script executed:

# Search for the hook implementation
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*useAllTranslations" | head -5

Repository: tolgee/figma-plugin

Length of output: 99


🏁 Script executed:

# Read the useAllTranslations hook implementation
cat -n src/ui/hooks/useAllTranslations.ts

Repository: tolgee/figma-plugin

Length of output: 3741


🏁 Script executed:

# Also check what fields/methods allTranslationsLoadable has
rg "\.getData|\.isLoading|\.clearCache" src/ui/views/Push/Push.tsx | head -10

Repository: tolgee/figma-plugin

Length of output: 344


🏁 Script executed:

# Verify if allTranslationsLoadable is used elsewhere and if including it in deps would cause issues
rg "allTranslationsLoadable" src/ui/views/Push/Push.tsx -A 1 -B 1 | head -30

Repository: tolgee/figma-plugin

Length of output: 644


🏁 Script executed:

# Check if nodesKey is properly derived from nodes to verify dependency coverage
grep -n "nodesKey" src/ui/views/Push/Push.tsx | head -10

Repository: tolgee/figma-plugin

Length of output: 111


🏁 Script executed:

# Check the full nodesKey definition and what it depends on
sed -n '74,85p' src/ui/views/Push/Push.tsx

Repository: tolgee/figma-plugin

Length of output: 545


🏁 Script executed:

# Check the full useEffect dependency array
sed -n '180,195p' src/ui/views/Push/Push.tsx

Repository: tolgee/figma-plugin

Length of output: 437


Address unstable allTranslationsLoadable dependency.

The effect uses allTranslationsLoadable.getData() (line 131) but allTranslationsLoadable is not in the dependency array. Since useAllTranslations() creates a new object on every render, this violates the exhaustive-deps rule and could cause stale closures. Adding it directly to dependencies would create an infinite loop due to the new object reference. Consider either:

  • Memoizing the return value of useAllTranslations() within the hook, or
  • Restructuring to avoid the object reference in the effect dependency.

Additionally, the configForDiff object (lines 147-152) redundantly spreads tolgeeConfig then re-adds the tags property that already exists in the spread.

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