Skip to content

fix(NES-1636): translate journey when copying to another team#9161

Open
jianwei1 wants to merge 7 commits into
mainfrom
jianweichong/nes-1636-fix-ai-translate-on-copy-to-team
Open

fix(NES-1636): translate journey when copying to another team#9161
jianwei1 wants to merge 7 commits into
mainfrom
jianweichong/nes-1636-fix-ai-translate-on-copy-to-team

Conversation

@jianwei1
Copy link
Copy Markdown
Contributor

@jianwei1 jianwei1 commented May 5, 2026

Summary

When a user copied a journey to another team and enabled the AI translation toggle, the journey was duplicated but never translated — it landed in the destination team in the source language, with no error or feedback. The dialog (CopyToTeamDialog) had been forwarding language and showTranslation to its submitAction since PR #6761, but the consumer in DuplicateJourneyMenuItem only consumed teamId and silently dropped the rest. TypeScript could not catch the structural mismatch.

After this fix, DuplicateJourneyMenuItem mirrors the working pattern from TranslateJourneyDialog: after journeyDuplicate resolves, useJourneyAiTranslateSubscription is started against the duplicated journey id and the user-selected target language. The subscription's onComplete closes the dialog, refetches template stats when applicable, and surfaces the success snackbar. The same-team direct-duplicate path (no dialog) and the translation-off path are unchanged.

Linear: NES-1636 — bug in Production
Plan: docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md

What changed

  • DuplicateJourneyMenuItem.handleDuplicateJourney now accepts (teamId?, language?, showTranslation?) and conditionally fires the AI translate subscription after journeyDuplicate resolves.
  • useJourneyAiTranslateSubscription is wired with onComplete (refetch template stats, success snackbar, close dialog, call handleCloseMenu) and onError (surface error snackbar with a t('Failed to translate journey') fallback when the upstream message is empty).
  • isTranslating and translationProgress are passed back to CopyToTeamDialog so its existing in-dialog progress UI renders during translation.
  • Source-language name derives from journey?.language.name on the prop, with useJourney() as fallback. The existing GetAdminJourneys query already returns language.name and title, so no GraphQL change.

Test coverage

4 new tests added under a new AI translation on copy to team describe block, plus 1 strengthened error-path assertion. All 10 tests pass.

Scenario What it pins
Translate-on happy path Duplicate runs, subscription starts with the duplicated id and selected target language, dialog closes after onComplete, success snackbar shows
Subscription error keeps the duplicate Error snackbar shows; handleCloseMenu and refetchTemplateStats are NOT called; the duplicate is not rolled back
fromTemplateId refetch on translate complete refetchTemplateStats is called with the template id after the subscription completes
journeyDuplicate error path "Failed to duplicate journey" snackbar shows; subscription is NOT invoked; handleCloseMenu is NOT called

The 5 existing tests (same-team direct-duplicate, dialog-shared-with-me path with and without journey prop, dialog cancel, refetchTemplateStats on direct duplicate) continue to pass unchanged, locking in the no-regression contract.

Residual review findings

ce-code-review autofix applied 3 safe local fixes (empty-error-message fallback, error-path assertion strengthening, duplicate-error-path test) and surfaced 16 residual findings recorded in docs/residual-review-findings/jianweichong-nes-1636-fix-ai-translate-on-copy-to-team.md for reviewer attention. Highlights:

  • Journeys API #1 (P1) Race: GetAdminJourneys refetch from updateLastActiveTeamId.onCompleted can unmount the menu item before the subscription's onComplete fires, leaving an orphan untranslated journey with no UI feedback. The same hazard exists in TranslateJourneyDialog — shared subscription-lifecycle debt.
  • use fastify #2 (P1) Cancel button mid-translation: dialog onClose only sets open=false but leaves translationVariables set, so the subscription keeps running and onComplete fires snackbar/handleCloseMenu on a dismissed dialog.
  • 14 P2/P3 items covering: missing client-side timeout, error-leaves-orphan-on-resubmit, snackbar wording inconsistency, journeyDuplicate: null partial response handling, TranslationVariables type duplication, and a state-machine refactor that would consolidate setLoading(false) calls scattered across 5 paths.

None of these block the user-visible bug fix; they document follow-up work the reviewer can weigh against scope.


Compound Engineering
Claude Code

Summary by CodeRabbit

  • New Features

    • Duplicate-to-team flow can optionally translate journeys during copy and shows translation progress/status in the copy dialog.
  • Bug Fixes

    • Ensures translation starts and completes correctly after duplication, surfaces success/error notifications, avoids starting translation on duplicate failure or missing IDs, and refetches template stats after translated copies.
  • Tests

    • Added/expanded tests covering translation-on-copy success, failures, progress, and edge cases; updated test imports.
  • Documentation

    • Added plan and residual-review notes detailing design, risks, and test scenarios.

jianwei1 added 3 commits May 5, 2026 20:07
CopyToTeamDialog forwarded language and showTranslation to its
submitAction, but DuplicateJourneyMenuItem only consumed teamId, so the
translation toggle never triggered the journeyAiTranslate subscription.
Mirror TranslateJourneyDialog: after journeyDuplicate resolves, start
the translation subscription with the user-selected target language and
the duplicated journey id, and let the subscription's onComplete close
the dialog and refetch template stats.
@linear
Copy link
Copy Markdown

linear Bot commented May 5, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Review Change Stack

Walkthrough

DuplicateJourneyMenuItem was extended to support an optional AI-translation step when copying a journey to another team: it captures translation variables, starts an AI translation subscription after duplication, surfaces progress into CopyToTeamDialog, shows completion/error snackbars, and preserves duplicate behavior when translation is disabled or fails.

Changes

AI Translation on Copy-to-Team

Layer / File(s) Summary
Plan & Context
docs/plans/.../2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md
Added implementation plan and requirements mirroring TranslateJourneyDialog wiring to start translation post-duplication.
Residual Review Findings
docs/residual-review-findings/.../jianweichong-nes-1636-fix-ai-translate-on-copy-to-team.md
Added residual findings listing prioritized issues, applied autofixes, and coverage notes.
Tests
apps/journeys-admin/src/components/.../DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx
Refactored testing-library import formatting, added SUPPORTED_LANGUAGE_IDS/subscription import, and appended an AI translation on copy to team test suite with GraphQL mocks and tests for success, subscription error, duplication error, missing id, and refetch behavior.
Imports & Types
apps/journeys-admin/src/components/.../DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
Added journey/team/context imports and internal JourneyLanguage/TranslationVariables helper types.
State & Subscription Setup
apps/journeys-admin/src/components/.../DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
Added translationVariables state and wired useJourneyAiTranslateSubscription with skip, plus onComplete/onError handlers to show snackbars, clear loading/translation state, close UI, and optionally refetch template stats.
Core Duplication Logic
apps/journeys-admin/src/components/.../DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
Extended handleDuplicateJourney to accept language and showTranslation; when showTranslation is requested and IDs exist, it populates translation variables and returns early so the subscription handles post-duplication work; otherwise proceeds with existing duplicate flow and refetch/snackbar/menu close.
Progress Wiring & UI Props
apps/journeys-admin/src/components/.../DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
Compute translationProgress from subscription data and pass isTranslating/translationProgress into CopyToTeamDialog; ensure onClose does not reset subscription-driven state.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as DuplicateJourneyMenuItem
    participant Dialog as CopyToTeamDialog
    participant API as GraphQL API
    participant Sub as AI Translate Subscription
    User->>UI: open menu, choose team, enable translation, pick language
    UI->>Dialog: open CopyToTeamDialog (isTranslating:true, progress:0)
    Dialog->>UI: confirm duplicate (teamId, language, showTranslation)
    UI->>API: mutation duplicateJourney(teamId,...)
    API-->>UI: duplicate success (newJourneyId)
    UI->>Sub: start AI translation subscription(variables)
    Sub-->>UI: progress events (percent, status)
    UI->>Dialog: update translationProgress
    alt translation completes
      Sub-->>UI: complete (translatedJourney)
      UI->>API: refetch template stats (if fromTemplateId)
      UI->>User: show success snackbar
      UI->>Dialog: close
    else subscription error
      Sub-->>UI: error
      UI->>User: show error snackbar (keep duplicate)
      UI->>Dialog: keep open / update state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: enabling AI translation when duplicating a journey to another team, which directly addresses the problem outlined in the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 jianweichong/nes-1636-fix-ai-translate-on-copy-to-team

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.

@jianwei1 jianwei1 self-assigned this May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Warnings
⚠️ ❗ Big PR (891 changes)

(change count - 891): Pull Request size seems relatively large. If Pull Request contains multiple changes, split each into separate PR will helps faster, easier review.

Generated by 🚫 dangerJS against 8d1a063

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 5, 2026

View your CI Pipeline Execution ↗ for commit 8d1a063

Command Status Duration Result
nx run journeys-admin-e2e:e2e ✅ Succeeded 27s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 10s View ↗
nx run-many --target=deploy --projects=journeys... ✅ Succeeded 2m 49s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-07 03:05:18 UTC

@github-actions github-actions Bot requested a deployment to Preview - journeys-admin May 5, 2026 20:41 Pending
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: 2

🧹 Nitpick comments (2)
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx (1)

25-33: ⚡ Quick win

Reuse the generated subscription variables type here.

TranslationVariables is a hand-rolled copy of the GraphQL variables contract for useJourneyAiTranslateSubscription. If the schema/codegen changes, this state can drift silently from the hook it feeds. Prefer importing the generated JourneyAiTranslateCreateSubscriptionVariables type instead of redefining it locally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`
around lines 25 - 33, TranslationVariables duplicates the GraphQL variables
shape for useJourneyAiTranslateSubscription; replace this hand-rolled interface
with the generated type JourneyAiTranslateCreateSubscriptionVariables by
importing it and using it wherever TranslationVariables is referenced (e.g., in
DuplicateJourneyMenuItem and any state/props tied to
useJourneyAiTranslateSubscription) so the component uses the codegen-backed
contract and won't drift from the hook's variables.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx (1)

950-973: ⚡ Quick win

Use the standard admin test harness for these new cases.

These added renders bypass the repo's required provider stack, so the translation-path tests are not exercising the component under the same theme/loading context as the rest of the app. Please wrap them with the shared ThemeProvider and ApolloLoadingProvider, ideally by moving all three setups through one helper so the harness stays consistent. As per coding guidelines: "apps/journeys-admin/**/*.spec.tsx: Tests must wrap components with required providers: SnackbarProvider → MockedProvider → ThemeProvider" and "Use ApolloLoadingProvider from test/ApolloLoadingProvider.tsx as a custom test provider."

Also applies to: 1128-1147, 1214-1234

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx`
around lines 950 - 973, The renderForTranslation helper renders
DuplicateJourneyMenuItem with MockedProvider and SnackbarProvider but omits the
repo-standard ThemeProvider and ApolloLoadingProvider; update
renderForTranslation (and the other similar helpers at the other ranges) to wrap
the component with the shared test harness: SnackbarProvider → MockedProvider →
ThemeProvider and include ApolloLoadingProvider (imported from
test/ApolloLoadingProvider.tsx) in the provider stack (or delegate to the
existing shared helper) so the tests use the same
ThemeProvider/ApolloLoadingProvider context as the rest of apps/journeys-admin
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`:
- Around line 116-137: When showTranslation is true but prerequisites
(language?.id, duplicatedJourneyId, or journeyData?.language) are missing, do
not fall through to the duplicate-success path; instead surface an explicit
error and stop the success flow. In the DuplicateJourneyMenuItem component check
these exact symbols (showTranslation, language?.id, duplicatedJourneyId,
journeyData?.language) and when any is missing call the existing error/snackbar
handler (or set an error state that triggers the error snackbar), ensure loading
is cleared, avoid calling setTranslationVariables or returning into the success
branch, and let the subscription onError/onComplete still handle cleanup
consistently.
- Around line 206-215: The dialog close handler only hides the dialog via
setOpen(false) but must also cancel and clear translation state so any in-flight
translation subscription stops and loading/snackbar side-effects cannot fire;
update the onClose passed to CopyToTeamDialog to call setOpen(false) and also
reset translationVariables (e.g., setTranslationVariables(null)/undefined), set
loading to false (setLoading(false)), clear translationProgress
(setTranslationProgress(0) or null) and invoke any existing cancel/unsubscribe
function (e.g., cancelTranslation() or unsubscribeTranslation()) so the
translation flow is detached when the user cancels.

---

Nitpick comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx`:
- Around line 950-973: The renderForTranslation helper renders
DuplicateJourneyMenuItem with MockedProvider and SnackbarProvider but omits the
repo-standard ThemeProvider and ApolloLoadingProvider; update
renderForTranslation (and the other similar helpers at the other ranges) to wrap
the component with the shared test harness: SnackbarProvider → MockedProvider →
ThemeProvider and include ApolloLoadingProvider (imported from
test/ApolloLoadingProvider.tsx) in the provider stack (or delegate to the
existing shared helper) so the tests use the same
ThemeProvider/ApolloLoadingProvider context as the rest of apps/journeys-admin
tests.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`:
- Around line 25-33: TranslationVariables duplicates the GraphQL variables shape
for useJourneyAiTranslateSubscription; replace this hand-rolled interface with
the generated type JourneyAiTranslateCreateSubscriptionVariables by importing it
and using it wherever TranslationVariables is referenced (e.g., in
DuplicateJourneyMenuItem and any state/props tied to
useJourneyAiTranslateSubscription) so the component uses the codegen-backed
contract and won't drift from the hook's variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f154bdca-fe32-49af-a9c1-78e0b9c85380

📥 Commits

Reviewing files that changed from the base of the PR and between dbbc1e3 and aa01f7e.

📒 Files selected for processing (4)
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
  • docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md
  • docs/residual-review-findings/jianweichong-nes-1636-fix-ai-translate-on-copy-to-team.md

@github-actions github-actions Bot had a problem deploying to Preview - journeys-admin May 5, 2026 20:48 Failure
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: 1

🧹 Nitpick comments (2)
docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md (2)

114-114: ⚡ Quick win

Clarify the source language name derivation logic.

The plan specifies using journey?.language.name.find(({ primary }) => !primary)?.value to derive the source language name, which filters for the non-primary name. This seems counterintuitive—typically the primary name is what should be displayed to users.

While the plan notes this mirrors TranslateJourneyDialog.tsx:159, it would be helpful to add a brief comment in the plan explaining why the non-primary name is preferred (e.g., if primary is a localized key and non-primary is the display name, or similar).

📝 Suggested clarification

Add a brief explanation after line 114:

 - Source-language-name derivation: pull from `journey?.language.name.find(({ primary }) => !primary)?.value ?? ''` (matches `TranslateJourneyDialog.tsx:159`).
+  Note: The non-primary name is used because [explain the reason - e.g., "primary contains the language code while non-primary contains the display name"].
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md`
at line 114, The plan's source-language-name derivation currently uses
journey?.language.name.find(({ primary }) => !primary)?.value (matching
TranslateJourneyDialog.tsx:159) which selects the non-primary name; update the
plan text to add a short explanatory comment after that line clarifying why the
non-primary name is preferred (e.g., primary is a localization key or internal
identifier while the non-primary entry holds the user-facing display name), and
mention that if journey is absent it falls back to useJourney() context to
mirror TranslateJourneyDialog behavior.

125-134: ⚡ Quick win

Add test scenario for dialog cancellation during active translation.

The test scenarios comprehensively cover happy paths, validation, and error conditions, but they don't address what happens if the user cancels the CopyToTeamDialog while the AI translation subscription is actively running.

This scenario is important because:

  1. It relates to the deferred question about handleCloseMenu timing (line 86).
  2. Without proper cleanup, a running subscription could continue writing to cache or showing snackbars after the dialog closes.
  3. It validates that translationVariables is properly reset and the subscription unsubscribes on unmount.
🧪 Suggested additional test scenario

Add to the test scenarios list:

- _Edge case — dialog cancelled mid-translation_ — Open Copy-to-Team dialog → select different team → toggle Translation on → select target language → submit. Mock subscription to emit progress but not complete. User clicks Cancel/closes dialog. Assert: subscription unsubscribes (via component unmount), no completion snackbar shows, duplicated journey persists (no rollback), `translationVariables` state is cleaned up.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md`
around lines 125 - 134, The tests are missing an edge case for cancelling
CopyToTeamDialog while an AI translation subscription is active; add a test that
opens CopyToTeamDialog, selects a different team, enables translation and a
target language, mocks JOUNREY_DUPLICATE to succeed and
JOUNREY_AI_TRANSLATE_CREATE_SUBSCRIPTION to emit progress but not complete, then
simulate user closing the dialog (unmount) and assert the subscription was
unsubscribed (no further emissions affect cache or snackbars), no completion
snackbar appears, duplicated journey remains, and component state
translationVariables is cleared; reference CopyToTeamDialog, handleCloseMenu,
translationVariables, JOURNEY_DUPLICATE and
JOURNEY_AI_TRANSLATE_CREATE_SUBSCRIPTION when locating code to mock and where to
assert cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md`:
- Line 86: Clarify and implement a deterministic fix for the race: in the code
path where handleDuplicateJourney triggers a translation subscription, defer
calling handleCloseMenu() until the subscription's onComplete handler fires
(i.e., only call handleCloseMenu from the translation subscription onComplete)
so TranslationDialogWrapper remains mounted during translation; alternatively,
if you choose Option B after testing, add an explicit comment in
handleDuplicateJourney and next to TranslationDialogWrapper explaining that
early unmounting does not cancel server-side translation and document why it’s
safe—ensure references to handleDuplicateJourney, handleCloseMenu,
TranslationDialogWrapper, and the subscription onComplete are used so reviewers
can find and verify the change.

---

Nitpick comments:
In `@docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md`:
- Line 114: The plan's source-language-name derivation currently uses
journey?.language.name.find(({ primary }) => !primary)?.value (matching
TranslateJourneyDialog.tsx:159) which selects the non-primary name; update the
plan text to add a short explanatory comment after that line clarifying why the
non-primary name is preferred (e.g., primary is a localization key or internal
identifier while the non-primary entry holds the user-facing display name), and
mention that if journey is absent it falls back to useJourney() context to
mirror TranslateJourneyDialog behavior.
- Around line 125-134: The tests are missing an edge case for cancelling
CopyToTeamDialog while an AI translation subscription is active; add a test that
opens CopyToTeamDialog, selects a different team, enables translation and a
target language, mocks JOUNREY_DUPLICATE to succeed and
JOUNREY_AI_TRANSLATE_CREATE_SUBSCRIPTION to emit progress but not complete, then
simulate user closing the dialog (unmount) and assert the subscription was
unsubscribed (no further emissions affect cache or snackbars), no completion
snackbar appears, duplicated journey remains, and component state
translationVariables is cleared; reference CopyToTeamDialog, handleCloseMenu,
translationVariables, JOURNEY_DUPLICATE and
JOURNEY_AI_TRANSLATE_CREATE_SUBSCRIPTION when locating code to mock and where to
assert cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6c803bac-c358-4f9d-b033-a28dde87e9bb

📥 Commits

Reviewing files that changed from the base of the PR and between aa01f7e and b65d9e2.

📒 Files selected for processing (2)
  • docs/plans/2026-05-05-001-fix-nes-1636-ai-translate-on-copy-to-team-plan.md
  • docs/residual-review-findings/jianweichong-nes-1636-fix-ai-translate-on-copy-to-team.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/residual-review-findings/jianweichong-nes-1636-fix-ai-translate-on-copy-to-team.md

@github-actions github-actions Bot requested a deployment to Preview - journeys-admin May 6, 2026 21:52 Pending
jianwei1 added a commit that referenced this pull request May 6, 2026
…og close

Address two CodeRabbit review comments on PR #9161:

1. When showTranslation is true but a prerequisite is missing
   (language.id, duplicatedJourneyId, journeyData.language), surface
   "Failed to translate journey" instead of falling through to the
   duplicate-only success snackbar. Recreating the silent-skip failure
   mode this PR was meant to fix would defeat the point.

2. The dialog's local onClose now also clears translationVariables and
   loading so cancelling cancels the in-flight translation subscription
   on the client. The duplicated journey already exists server-side; the
   user finds it under the destination team next time they navigate.

Adds a unit test covering the new defensive branch (journeyDuplicate
returns null with showTranslation enabled).
@github-actions github-actions Bot had a problem deploying to Preview - journeys-admin May 6, 2026 21:59 Failure
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: 1

🧹 Nitpick comments (2)
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx (1)

1085-1088: 💤 Low value

refetchTemplateStats assertion is trivially true here.

This test renders DuplicateJourneyMenuItem without fromTemplateId, so refetchTemplateStats cannot be called via the onComplete branch regardless of the error path. To meaningfully assert "no stats refetch on subscription error", add fromTemplateId="templateId123" to the rendered component (or move this assertion into a variant test that does pass it). As written it doesn't protect against a regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx`
around lines 1085 - 1088, The test currently asserts that refetchTemplateStats
is not called on subscription error but renders DuplicateJourneyMenuItem without
a fromTemplateId so refetchTemplateStats could never be invoked; update the test
render of DuplicateJourneyMenuItem to include a fromTemplateId (e.g.
"templateId123") so the onComplete branch that may call refetchTemplateStats is
reachable (or alternatively move the refetchTemplateStats assertion into a
separate variant test that renders with fromTemplateId), keeping assertions
against handleCloseMenu and refetchTemplateStats unchanged.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx (1)

19-33: ⚡ Quick win

Import JourneyLanguage from CopyToTeamDialog to avoid duplicate type definitions.

Both CopyToTeamDialog and DuplicateJourneyMenuItem define identical JourneyLanguage interfaces with the same fields and optionality (id: string, localName?: string, nativeName?: string). The fallback logic at Line 143 (language.nativeName ?? language.localName ?? '') works correctly with both. However, importing the type from @core/journeys/ui/CopyToTeamDialog eliminates duplication and prevents future divergence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`
around lines 19 - 33, Duplicate JourneyLanguage definition found in
DuplicateJourneyMenuItem.tsx; import the type from
`@core/journeys/ui/CopyToTeamDialog` instead of redefining it. Remove the local
JourneyLanguage interface in DuplicateJourneyMenuItem and add an import for
JourneyLanguage from CopyToTeamDialog, ensuring existing code that uses
JourneyLanguage (including the fallback logic using language.nativeName ??
language.localName ?? '') continues to type-check and compile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`:
- Around line 135-146: The code computes sourceLanguageName using
journeyData.language.name.find(({ primary }) => !primary)?.value ?? '' which
returns empty when only a primary entry exists; change the logic in
DuplicateJourneyMenuItem to first try a non-primary name then fall back to the
primary name's value (e.g., check for a primary entry and use its .value if no
non-primary found) before calling setTranslationVariables so journeyLanguageName
and userLanguageName are populated; update the tests that assert
journeyLanguageName: '' (lines referenced in the review) to expect the primary
language value instead.

---

Nitpick comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx`:
- Around line 1085-1088: The test currently asserts that refetchTemplateStats is
not called on subscription error but renders DuplicateJourneyMenuItem without a
fromTemplateId so refetchTemplateStats could never be invoked; update the test
render of DuplicateJourneyMenuItem to include a fromTemplateId (e.g.
"templateId123") so the onComplete branch that may call refetchTemplateStats is
reachable (or alternatively move the refetchTemplateStats assertion into a
separate variant test that renders with fromTemplateId), keeping assertions
against handleCloseMenu and refetchTemplateStats unchanged.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`:
- Around line 19-33: Duplicate JourneyLanguage definition found in
DuplicateJourneyMenuItem.tsx; import the type from
`@core/journeys/ui/CopyToTeamDialog` instead of redefining it. Remove the local
JourneyLanguage interface in DuplicateJourneyMenuItem and add an import for
JourneyLanguage from CopyToTeamDialog, ensuring existing code that uses
JourneyLanguage (including the fallback logic using language.nativeName ??
language.localName ?? '') continues to type-check and compile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a82ea935-1ab9-463a-86ff-e26d748aca86

📥 Commits

Reviewing files that changed from the base of the PR and between b65d9e2 and 0340c29.

📒 Files selected for processing (2)
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx

Comment on lines +135 to 146
const sourceLanguageName =
journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''

setTranslationVariables({
journeyId: duplicatedJourneyId,
name: journeyData.title ?? '',
journeyLanguageName: sourceLanguageName,
textLanguageId: language.id,
textLanguageName: language.nativeName ?? language.localName ?? '',
userLanguageId: journeyData.language.id,
userLanguageName: sourceLanguageName
})
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 6, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sourceLanguageName silently becomes empty for source languages that only expose a primary name entry.

find(({ primary }) => !primary)?.value ?? '' only returns the localized (non-primary) name. For a journey whose language.name only has the primary entry (e.g., English with [{ value: 'English', primary: true }]), this becomes '' and is then sent as both journeyLanguageName and userLanguageName to the AI translation subscription — losing useful prompt context for the translator. Falling back to the primary value preserves the language name in this case.

Note the existing tests at Lines 982-988 / 1108-1114 explicitly assert journeyLanguageName: '', so they will need to be updated alongside this change.

🛡️ Suggested fallback
-        const sourceLanguageName =
-          journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''
+        const sourceLanguageName =
+          journeyData.language.name.find(({ primary }) => !primary)?.value ??
+          journeyData.language.name.find(({ primary }) => primary)?.value ??
+          ''
📝 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 sourceLanguageName =
journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''
setTranslationVariables({
journeyId: duplicatedJourneyId,
name: journeyData.title ?? '',
journeyLanguageName: sourceLanguageName,
textLanguageId: language.id,
textLanguageName: language.nativeName ?? language.localName ?? '',
userLanguageId: journeyData.language.id,
userLanguageName: sourceLanguageName
})
const sourceLanguageName =
journeyData.language.name.find(({ primary }) => !primary)?.value ??
journeyData.language.name.find(({ primary }) => primary)?.value ??
''
setTranslationVariables({
journeyId: duplicatedJourneyId,
name: journeyData.title ?? '',
journeyLanguageName: sourceLanguageName,
textLanguageId: language.id,
textLanguageName: language.nativeName ?? language.localName ?? '',
userLanguageId: journeyData.language.id,
userLanguageName: sourceLanguageName
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`
around lines 135 - 146, The code computes sourceLanguageName using
journeyData.language.name.find(({ primary }) => !primary)?.value ?? '' which
returns empty when only a primary entry exists; change the logic in
DuplicateJourneyMenuItem to first try a non-primary name then fall back to the
primary name's value (e.g., check for a primary entry and use its .value if no
non-primary found) before calling setTranslationVariables so journeyLanguageName
and userLanguageName are populated; update the tests that assert
journeyLanguageName: '' (lines referenced in the review) to expect the primary
language value instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping for scope. The sourceLanguageName derivation is a deliberate mirror of TranslateJourneyDialog.tsx:158-159:

const currentLanguageName =
  journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''

That pattern has been in production since the AI translation feature shipped — same fall-through-to-empty for journeys whose language.name only carries a primary entry. NES-1636 was deliberately framed as a near-mechanical port of TranslateJourneyDialog's wiring (per the plan's "Mirror TranslateJourneyDialog's subscription wiring rather than introducing a shared hook" decision), so fixing this here only would diverge from the reference pattern: same input would now produce different translation prompts depending on which entry point fired.

The right shape for this fix is a follow-up that updates both callsites and their tests in lockstep — happy to file it as a separate ticket if translation quality from missing source-language context is showing up in QA. For this PR I'd rather hold the symmetry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jianwei1, that's a well-reasoned argument — the symmetry concern is valid. Having both entry points (DuplicateJourneyMenuItem and TranslateJourneyDialog) derive sourceLanguageName identically ensures the same journey produces the same translation prompt regardless of which path is taken. Fixing one in isolation would silently introduce a behavioral split that could be harder to debug later.

If you'd like, I can open a follow-up GitHub issue to track updating both callsites and their tests in lockstep. Just let me know.

(´・ω・`) noted for future reviews


✏️ Learnings added
Learnt from: jianwei1
Repo: JesusFilm/core PR: 9161
File: apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx:135-146
Timestamp: 2026-05-06T22:25:57.396Z
Learning: In `apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`, the `sourceLanguageName` derivation `journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''` is a deliberate mirror of the same pattern in `TranslateJourneyDialog.tsx` (around line 158-159: `journeyData.language.name.find(({ primary }) => !primary)?.value ?? ''`). Both callsites intentionally fall through to an empty string for journeys whose `language.name` only carries a primary entry, to maintain symmetry between entry points. Any fix must update both callsites and their tests in lockstep to avoid behavioral divergence. Do not flag this as a bug in either file independently.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jianwei1
Repo: JesusFilm/core PR: 8998
File: apps/journeys-admin/src/components/Editor/Slider/Settings/CanvasDetails/JourneyAppearance/Chat/ChatOption/Summary/Summary.tsx:68-130
Timestamp: 2026-04-09T02:33:33.597Z
Learning: In this repo’s editor command implementations that use `useCommand().add()` (e.g., command hooks/components like `useBlockCreateCommand`, `useBlockDeleteCommand`, `MoveBlock`, property controls, etc.), follow the established pattern: call mutation/fetch functions as fire-and-forget using `void` inside both `execute()` and `undo()` callbacks, and register the command synchronously via `CommandProvider.add()` (there’s no async registration path). If an undo/execute race could occur due to a closure variable not being set yet (e.g., `createdButtonId`), guard with an early null/undefined check (e.g., `if (createdButtonId == null) return`). Do not flag the `void` fire-and-forget usage or synchronous command registration as bugs in future reviews when this pattern (including the null-check guard) is present.

…ssing

Address CodeRabbit feedback on PR #9161 (apps/.../DuplicateJourneyMenuItem.tsx
line 137): when showTranslation is true but a prerequisite is missing
(language.id, duplicatedJourneyId, journeyData.language), surface
"Failed to translate journey" instead of falling through to the
duplicate-only success snackbar. Recreating the silent-skip failure mode
this PR was meant to fix would defeat the point. Yup blocks the common
"no language selected" case before submit; this branch is a defensive
backstop for partial GraphQL responses (e.g. gateway returning
{ data: { journeyDuplicate: null } } despite codegen marking the field
non-null) and for callers without a journey context.

Adds a unit test covering the new defensive branch.

Skipped the second CodeRabbit comment (line 215, "Closing the dialog does
not cancel the in-flight translation state"). TranslationDialogWrapper
hides the Cancel and Create buttons while loading is true, and
CopyToTeamDialog blocks backdrop click and Escape during translation, so
onClose is not user-reachable while translationVariables is set.
@jianwei1 jianwei1 force-pushed the jianweichong/nes-1636-fix-ai-translate-on-copy-to-team branch from 0340c29 to 8781f61 Compare May 6, 2026 22:21
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 6, 2026 22:22 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Thu May 7 15:02:13 NZST 2026

@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 7, 2026 02:57 Inactive
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`:
- Around line 102-133: Pre-validate translation prerequisites before calling
journeyDuplicate in DuplicateJourneyMenuItem: check showTranslation and ensure
language?.id and journeyData?.language are present (and return early with
enqueueSnackbar(t('Failed to translate'), { variant: 'error', preventDuplicate:
true }) and setLoading(false)) before invoking journeyDuplicate; only call
journeyDuplicate when prerequisites pass (use targetTeamId resolution as before)
so you don't create an untranslated duplicate then surface a translation error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1137e2ac-e0e0-4c12-995a-877f69d424fd

📥 Commits

Reviewing files that changed from the base of the PR and between 0340c29 and 8d1a063.

📒 Files selected for processing (2)
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx

Comment on lines 102 to +133
setLoading(true)
try {
if (teamId != null) {
await journeyDuplicate({
variables: {
id,
teamId
}
})
} else if (activeTeam?.id != null) {
await journeyDuplicate({
variables: { id, teamId: activeTeam.id }
const targetTeamId = teamId ?? activeTeam?.id
if (targetTeamId == null) {
setLoading(false)
return
}

const { data: duplicateData } = await journeyDuplicate({
variables: { id, teamId: targetTeamId }
})

const duplicatedJourneyId = duplicateData?.journeyDuplicate?.id

if (showTranslation === true) {
if (
language?.id == null ||
duplicatedJourneyId == null ||
journeyData?.language == null
) {
// Translation was explicitly requested but a prerequisite is missing
// (Yup normally blocks this — defensive against partial GraphQL
// responses or callers without a journey context). Surface an
// explicit error rather than falling through to the duplicate-only
// success snackbar.
enqueueSnackbar(t('Failed to translate journey'), {
variant: 'error',
preventDuplicate: true
})
setLoading(false)
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pre-validate translation prerequisites before duplicating.

At Line 110, duplication runs before validating language?.id and journeyData?.language. In the missing-prereq path, this can still create a duplicate and then report translation failure, leaving an unintended untranslated copy.

Suggested minimal change
   setLoading(true)
   try {
+    if (showTranslation === true) {
+      if (language?.id == null || journeyData?.language == null) {
+        enqueueSnackbar(t('Failed to translate journey'), {
+          variant: 'error',
+          preventDuplicate: true
+        })
+        setLoading(false)
+        return
+      }
+    }
+
     const targetTeamId = teamId ?? activeTeam?.id
     if (targetTeamId == null) {
       setLoading(false)
       return
     }
@@
-    if (showTranslation === true) {
-      if (
-        language?.id == null ||
-        duplicatedJourneyId == null ||
-        journeyData?.language == null
-      ) {
+    if (showTranslation === true) {
+      if (duplicatedJourneyId == null) {
         enqueueSnackbar(t('Failed to translate journey'), {
           variant: 'error',
           preventDuplicate: true
         })
         setLoading(false)
         return
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.tsx`
around lines 102 - 133, Pre-validate translation prerequisites before calling
journeyDuplicate in DuplicateJourneyMenuItem: check showTranslation and ensure
language?.id and journeyData?.language are present (and return early with
enqueueSnackbar(t('Failed to translate'), { variant: 'error', preventDuplicate:
true }) and setLoading(false)) before invoking journeyDuplicate; only call
journeyDuplicate when prerequisites pass (use targetTeamId resolution as before)
so you don't create an untranslated duplicate then surface a translation error.

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