Skip to content

refactor: translate journey dialog#6742

Merged
edmonday merged 6 commits into
feature/25-04-MA-feat-ai-translationfrom
edmondshen/nes-244-refactor-translate-journey-dialog
May 23, 2025
Merged

refactor: translate journey dialog#6742
edmonday merged 6 commits into
feature/25-04-MA-feat-ai-translationfrom
edmondshen/nes-244-refactor-translate-journey-dialog

Conversation

@edmonday
Copy link
Copy Markdown
Contributor

@edmonday edmonday commented May 22, 2025

This PR refactors the TranslateJourneyDialog to make use of the useJourneyDuplicateAndTranslate custom hook. It also prevents closing of the dialog when translating similar to the CopyToTeamDialog.

Updated the custom hook to handle and return the loading state.

Summary by CodeRabbit

  • New Features

    • Added AI-powered journey translation, allowing users to create translated copies of journeys in supported languages.
    • Introduced a "Translate" menu option in the Journey Card menu for easy access to translation functionality.
    • Added a dialog interface for selecting target language and initiating journey translation.
    • Enhanced journey duplication to optionally include translation during the copy process.
    • Added language detection capabilities for journeys using AI.
  • Improvements

    • Expanded language selection with error handling and validation in dialogs.
    • Updated UI components to better support translation workflows, including loading states and feedback messages.
    • Added new localization strings for translation and duplication feedback.
  • Bug Fixes

    • Improved test coverage and reliability for journey translation and duplication features.
    • Enhanced dialog and menu interactions for smoother user experience.
  • Chores

    • Added new dependencies to support AI and Google model integrations.

@edmonday edmonday requested a review from mikeallisonJS May 22, 2025 23:57
@edmonday edmonday self-assigned this May 22, 2025
@linear
Copy link
Copy Markdown

linear Bot commented May 22, 2025

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This update introduces comprehensive AI-powered journey translation and language detection features across the backend and frontend. It extends GraphQL schemas, adds Prisma model fields, and implements new mutations for translation and language detection. The frontend gains new dialogs, hooks, and UI controls for translating and copying journeys, with enhanced localization and prompt security mechanisms. Supporting utilities and tests are included throughout.

Changes

File(s) / Area Change Summary
apis/api-gateway/schema.graphql, apis/api-journeys-modern/schema.graphql GraphQL schemas extended for journey translation, language detection, multi-graph support, new scalars, enums, types, and mutations.
apis/api-journeys-modern/db/schema.prisma Added optional fields to Block model: placeholder, required, gap, submitEnabled.
apis/api-journeys-modern/infrastructure/locals.tf Added GOOGLE_GENERATIVE_AI_API_KEY to environment variables.
apis/api-journeys-modern/src/lib/auth/ability.ts New authorization utility for journey actions and subjects.
apis/api-journeys-modern/src/lib/redisConnection.ts New Redis connection config module.
apis/api-journeys-modern/src/schema/builder.ts Added DateTime and JourneyStatus scalars, input plugin, scalar registration.
apis/api-journeys-modern/src/schema/journey/index.ts Imported journey schema module.
apis/api-journeys-modern/src/schema/journey/journey.acl.ts, journey.acl.spec.ts New journey access control logic and comprehensive tests for role-based permissions.
apis/api-journeys-modern/src/schema/journey/journey.ts New federated Journey type with Prisma integration and field resolvers.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/*.ts New utilities to extract and format journey block content for translation, including handling images, videos, buttons, radio questions, and text responses.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts New utility to fetch language names via GraphQL.
apis/api-journeys-modern/src/schema/journeyAiTranslate/index.ts Import for journey AI translation module.
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts New mutation to translate journeys using AI, updating journey and block content, with streaming and error handling.
apis/api-journeys-modern/src/schema/journeyLanguageAiDetect/index.ts Import for journey language AI detection module.
apis/api-journeys-modern/src/schema/journeyLanguageAiDetect/journeyLanguageAiDetect.ts New mutation to detect journey content language using AI.
apis/api-journeys-modern/src/schema/schema.ts Imported new journey, translation, and language detection modules.
apis/api-journeys-modern/src/schema/journeyEventsExportLog/journeyEventsExportLog.spec.ts Refactored test suite for journey events export log with improved mocking.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/*.tsx Enhanced menu to include "Translate" option, updated props and tests, adjusted menu item logic.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/JourneyCardMenu*.tsx Added state and logic for opening the Translate dialog, dynamic import of dialog, updated tests.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/* New dialog component for translating journeys, with storybook and tests.
apps/journeys-admin/src/components/Team/CopyToTeamMenuItem/CopyToTeamMenuItem*.tsx Refactored to use new duplication-and-translate hook, updated props, improved test coverage.
libs/journeys/ui/src/components/Card/Card.spec.tsx, .../SearchBar/SearchBar.spec.tsx, .../TemplateGallery/* Test timeouts and minor adjustments for async reliability.
libs/journeys/ui/src/components/CopyToTeamDialog/CopyToTeamDialog*.tsx Added translation option, language selector, validation, and error handling to dialog and tests.
libs/journeys/ui/src/components/TemplateView/CreateJourneyButton/CreateJourneyButton.tsx Switched to new duplication-and-translate hook, updated handler signatures and dialog logic.
libs/journeys/ui/src/components/TranslationDialogWrapper/* New dialog wrapper component for translation workflows, with tests and re-export.
libs/journeys/ui/src/libs/useJourneyAiTranslateMutation/* New hook for AI journey translation mutation, supported languages list, tests, and re-export.
libs/journeys/ui/src/libs/useJourneyDuplicateAndTranslate/* New hook for duplicating and optionally translating journeys, with re-export.
libs/locales/en/apps-journeys-admin.json, .../apps-journeys-ui.json, .../libs-journeys-ui.json Added and reorganized localization strings for translation and copying features.
libs/shared/ai/src/getImageDescription/* New utility to generate image descriptions using AI, with re-export.
libs/shared/ai/src/prompts/hardeningPrompt/, .../preSystemPrompt, .../index.ts, .../system.ts New prompt hardening utilities and security instructions, replaced system prompt export, added tests.
libs/shared/ui/src/components/LanguageAutocomplete/LanguageAutocomplete.tsx Added error prop and conditional error styling to language autocomplete component.
package.json Added @ai-sdk/google and ai dependencies.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AdminUI
    participant Backend(GQL)
    participant AIService
    participant DB

    User->>AdminUI: Click "Translate" on journey
    AdminUI->>AdminUI: Open TranslateJourneyDialog
    User->>AdminUI: Select language, confirm translation
    AdminUI->>Backend(GQL): journeyAiTranslateCreate(input)
    Backend(GQL)->>DB: Fetch journey and blocks
    Backend(GQL)->>AIService: Send translation prompt (title, desc, blocks)
    AIService-->>Backend(GQL): Return translated content
    Backend(GQL)->>DB: Update journey and blocks with translations
    Backend(GQL)-->>AdminUI: Return updated journey
    AdminUI->>User: Show translated journey
Loading
sequenceDiagram
    participant User
    participant AdminUI
    participant Backend(GQL)
    participant AIService
    participant DB

    User->>AdminUI: Click "Copy to Team", enable translation
    AdminUI->>AdminUI: Show CopyToTeamDialog with translation options
    User->>AdminUI: Select team and language, confirm
    AdminUI->>Backend(GQL): Duplicate journey
    Backend(GQL)->>DB: Duplicate journey and blocks
    Backend(GQL)-->>AdminUI: Return duplicated journey ID
    AdminUI->>Backend(GQL): journeyAiTranslateCreate(input) (if translation enabled)
    Backend(GQL)->>DB: Fetch duplicated journey and blocks
    Backend(GQL)->>AIService: Send translation prompt
    AIService-->>Backend(GQL): Return translated content
    Backend(GQL)->>DB: Update duplicated journey and blocks
    Backend(GQL)-->>AdminUI: Return translated journey
    AdminUI->>User: Show confirmation and translated journey
Loading

Possibly related PRs

Suggested reviewers

  • mikeallisonJS

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@edmonday edmonday changed the base branch from main to feature/25-04-MA-feat-ai-translation May 22, 2025 23:57
@github-actions github-actions Bot temporarily deployed to Preview - journeys May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - videos-admin May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - docs May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - short-links May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - arclight May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 22, 2025 23:59 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - watch May 22, 2025 23:59 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

The latest updates on your projects.

Name Status Preview Updated (UTC)
docs ✅ Ready docs preview Fri May 23 12:00:15 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

The latest updates on your projects.

Name Status Preview Updated (UTC)
arclight ✅ Ready arclight preview Fri May 23 12:00:57 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

The latest updates on your projects.

Name Status Preview Updated (UTC)
short-links ✅ Ready short-links preview Fri May 23 12:01:31 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys ✅ Ready journeys preview Fri May 23 14:58:43 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Fri May 23 12:13:52 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
videos-admin ✅ Ready videos-admin preview Fri May 23 14:58:17 NZST 2025

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Fri May 23 14:59:17 NZST 2025

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (29)
libs/journeys/ui/src/components/SearchBar/SearchBar.spec.tsx (1)

140-140: 🛠️ Refactor suggestion

Test description and implementation mismatch

The test is titled "should have language icon" but is now checking for the SearchBar element itself rather than a language or globe icon. Consider updating the test name to match what it's actually testing.

-  it('should have language icon', async () => {
+  it('should render SearchBar component', async () => {
📝 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.

  it('should render SearchBar component', async () => {
    expect(screen.getByTestId('SearchBar')).toBeInTheDocument()
  })
🤖 Prompt for AI Agents
In libs/journeys/ui/src/components/SearchBar/SearchBar.spec.tsx at line 140, the
test name "should have language icon" does not match the assertion which checks
for the presence of the SearchBar element. Update the test name to accurately
describe that it is verifying the SearchBar element is in the document, or
alternatively, modify the test to check for the language or globe icon if that
was the original intent.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getButtonBlockContent.ts (1)

1-9: 💡 Verification agent

🧩 Analysis chain

Potential issue with accessing block.label property

The function attempts to access block.label on line 7, but based on the provided code snippets, the Block type might not have a label property defined. This could lead to accessing an undefined property.

Consider adding type safety by checking if the label exists before using it:

export function getButtonBlockContent({ block }: { block: Block }): string {
  return `
## Button:
- Block ID: ${block.id}
-  - Button Label: ${block.label}
+  - Button Label: ${block.label ?? 'No label'}
`
}

🏁 Script executed:

#!/bin/bash
# Check if the Block type in Prisma schema has a label property
echo "Checking if Block type has label property in Prisma schema..."
fd --type f --extension prisma | xargs grep -l "model Block" | xargs cat

# Also check TypeScript interfaces/types for Block
echo "Checking TypeScript interfaces/types for Block..."
fd --type f --extension ts --extension tsx | xargs grep -l "type Block" | xargs cat
fd --type f --extension ts --extension tsx | xargs grep -l "interface Block" | xargs cat

Length of output: 108280


Handle optional label property on Block

The Block type generated by Prisma includes label?: string | null, so accessing block.label will never be a compile-time error but may produce "null" at runtime if it’s not set. It’s best to provide a fallback:

• File:
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getButtonBlockContent.ts

Suggested change:

 export function getButtonBlockContent({ block }: { block: Block }): string {
-  return `
-## Button:
-- Block ID: ${block.id}
-- Button Label: ${block.label}
-`
-}
+  const label = block.label ?? 'No label';
+  return `
+## Button:
+- Block ID: ${block.id}
+- Button Label: ${label}
+`;
+}
📝 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.

import { Block } from '.prisma/api-journeys-modern-client'

export function getButtonBlockContent({ block }: { block: Block }): string {
  const label = block.label ?? 'No label';
  return `
## Button:
- Block ID: ${block.id}
- Button Label: ${label}
`;
}
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getButtonBlockContent.ts
around lines 1 to 9, the function accesses block.label which may be undefined or
null according to the Block type. To fix this, update the code to safely handle
the optional label property by providing a fallback value (e.g., an empty string
or a placeholder) when block.label is null or undefined before including it in
the returned string.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getRadioQuestionBlockContent.ts (1)

3-23: 🛠️ Refactor suggestion

Add null/undefined handling for block labels.

The function should handle cases where block.label or question.label might be null or undefined to prevent "undefined" from appearing in the output string.

Consider adding null checks:

- - Text: ${block.label}
+ - Text: ${block.label ?? ''}

- result += `- ${question.label}\n`
+ result += `- ${question.label ?? ''}\n`
📝 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.

export function getRadioQuestionBlockContent({
  blocks,
  block
}: {
  blocks: Block[]
  block: Block
}): string {
  let result = `
## Radio Question List:
- Block ID: ${block.id}
- Text: ${block.label ?? ''}
### Questions:
`
  const questions = blocks.filter(
    (childBlock) => childBlock.typename === 'RadioQuestionBlock'
  )
  for (const question of questions) {
    result += `- ${question.label ?? ''}\n`
  }
  return result
}
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getRadioQuestionBlockContent.ts
between lines 3 and 23, the function does not handle cases where block.label or
question.label might be null or undefined, which can cause "undefined" to appear
in the output string. Update the code to check if block.label and question.label
are null or undefined and replace them with an empty string or a default
placeholder before including them in the result string.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getTextResponseBlockContent.ts (1)

10-16: 🛠️ Refactor suggestion

Avoid type assertions for Block properties.

Using (block as any) to access properties suggests a typing issue. Consider updating the Block type or creating a more specific interface.

-  - Input Placeholder Text: ${(block as any).placeholder ?? ''}
-  - Required: ${(block as any).required ?? false}
+  - Input Placeholder Text: ${block.placeholder ?? ''}
+  - Required: ${block.required ?? false}

This requires updating the Block type definition or extending it:

interface TextResponseBlock extends Block {
  placeholder?: string
  required?: boolean
}
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getTextResponseBlockContent.ts
around lines 10 to 16, avoid using type assertions like (block as any) to access
placeholder and required properties. Instead, define a more specific interface
extending Block (e.g., TextResponseBlock) that includes these optional
properties, then update the function to use this interface for the block
parameter to ensure proper typing without assertions.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.spec.tsx (2)

45-46: 🛠️ Refactor suggestion

Expand test coverage beyond the TODO comment

As noted in the comment, additional tests are needed. Consider adding tests for:

  1. Language selection functionality
  2. The Create button and actual translation process
  3. Loading state during translation
  4. Error handling

These would provide more comprehensive coverage of the component's behavior.

it('should call duplicateAndTranslate when Create is clicked with a selected language', async () => {
  // Mock implementation
  const handleClose = jest.fn()
  const duplicateAndTranslateMock = jest.fn().mockResolvedValue(undefined)
  
  jest.mock('@core/journeys/ui/libs/useJourneyDuplicateAndTranslate', () => ({
    useJourneyDuplicateAndTranslate: () => ({
      duplicateAndTranslate: duplicateAndTranslateMock,
      loading: false
    })
  }))

  render(
    <MockedProvider mocks={[getLanguagesMock]}>
      <JourneyProvider value={{ journey: defaultJourney }}>
        <TranslateJourneyDialog open={true} onClose={handleClose} />
      </JourneyProvider>
    </MockedProvider>
  )

  // Select a language (implementation depends on your component)
  // ...

  fireEvent.click(screen.getByText('Create'))
  expect(duplicateAndTranslateMock).toHaveBeenCalled()
})
🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.spec.tsx
around lines 45 to 46, the test coverage is minimal with only a placeholder
comment. Expand the tests by adding cases that verify language selection
functionality, simulate clicking the Create button to ensure the
duplicateAndTranslate function is called, test the loading state during
translation, and handle error scenarios. Mock necessary hooks and functions,
render the component with required providers, simulate user interactions like
selecting a language and clicking buttons, and assert expected behaviors to
provide comprehensive coverage.

10-11: 🛠️ Refactor suggestion

Ensure the Media Query mock is properly implemented

The file mocks useMediaQuery but doesn't provide an implementation. Consider adding a mock implementation to ensure consistent test behavior.

jest.mock('@mui/material/useMediaQuery')
+jest.mock('@mui/material/useMediaQuery', () => ({
+  __esModule: true,
+  default: () => true // or a function that returns the desired 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.

jest.mock('@mui/material/useMediaQuery')
jest.mock('@mui/material/useMediaQuery', () => ({
  __esModule: true,
  default: () => true // or a function that returns the desired value
}))
🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.spec.tsx
at lines 10 to 11, the jest.mock call for '@mui/material/useMediaQuery' lacks a
mock implementation. Add a mock implementation for useMediaQuery that returns a
consistent boolean value to ensure predictable and stable test behavior across
different environments.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getVideoBlockContent.ts (2)

17-26: 🛠️ Refactor suggestion

Add null check for empty src strings

The current null check for videoCoverBlock.src might not handle empty strings correctly. If src is an empty string, it will pass the null check but could cause issues with the image description service.

-  if (videoCoverBlock && videoCoverBlock.src != null) {
+  if (videoCoverBlock && videoCoverBlock.src != null && videoCoverBlock.src !== '') {
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getVideoBlockContent.ts
around lines 17 to 26, the check for videoCoverBlock.src only verifies if it is
not null but does not handle empty strings. Update the condition to also check
that videoCoverBlock.src is not an empty string before calling
getImageDescription, ensuring the image description service is not called with
invalid input.

14-16: 💡 Verification agent

🧩 Analysis chain

Verify Block type has posterBlockId property

The code looks for a posterBlockId property on the Block type, but this property isn't visible in the provided Block type definition from the relevant snippets. Ensure this property exists in the Block type to avoid runtime errors.


🏁 Script executed:

#!/bin/bash
# Check if posterBlockId exists in the Block type definition

# Search for Block type definition with posterBlockId
echo "Searching for posterBlockId in Block type definition..."
rg -A 15 "type Block" --type=ts

# Search for posterBlockId in Prisma schema
echo "Searching for posterBlockId in Prisma schema..."
fd "schema.prisma" -x cat {} | grep -A 20 "model Block" 

Length of output: 65190


Replace block.posterBlockId with the existing block.coverBlockId

The Block model (Prisma and generated GraphQL types) defines coverBlockId, not posterBlockId. Referencing a non-existent posterBlockId will lead to undefined at runtime.

• File: apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getVideoBlockContent.ts
Lines 14–16

Suggested change:

- const videoCoverBlock = blocks.find(
-   (childBlock) => childBlock.id === block.posterBlockId
- )
+ const videoCoverBlock = blocks.find(
+   (childBlock) => childBlock.id === block.coverBlockId
+ )
📝 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.

  const videoCoverBlock = blocks.find(
    (childBlock) => childBlock.id === block.coverBlockId
  )
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getVideoBlockContent.ts
around lines 14 to 16, the code incorrectly references block.posterBlockId which
does not exist on the Block type. Replace block.posterBlockId with
block.coverBlockId to match the actual property defined in the Block model and
avoid runtime errors.
apis/api-journeys-modern/src/schema/builder.ts (2)

119-119: ⚠️ Potential issue

Missing JourneyStatus scalar registration

While you've added JourneyStatus as a scalar type, there's no corresponding addScalarType call for it like there is for the other scalar types. Either add a registration call or remove the scalar definition if it's intended to be an enum.

#!/bin/bash
# Verify if JourneyStatus is intended to be a scalar or enum

echo "Checking for JourneyStatus usage in schema files..."
rg "JourneyStatus" --type=graphql

echo "Checking how JourneyStatus is used in code..."
rg "JourneyStatus" --type=ts --glob=!**/__generated__/**
🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/builder.ts at line 119, the JourneyStatus
scalar type is missing registration with addScalarType like other scalars such
as DateTime. Verify if JourneyStatus is intended as a scalar or enum; if scalar,
add a builder.addScalarType('JourneyStatus', JourneyStatusResolver) call to
register it properly. If it is an enum, remove the scalar definition to avoid
confusion and ensure consistent schema usage.

67-70: ⚠️ Potential issue

Inconsistency between JourneyStatus definitions

There's an inconsistency in how JourneyStatus is defined. In builder.ts it's defined as a scalar type, but in journey.ts it's defined as an enum type. This could lead to schema inconsistencies or runtime errors.

  Scalars: {
    Date: { Input: Date; Output: Date }
    DateTimeISO: { Input: Date; Output: Date }
    DateTime: { Input: Date; Output: Date }
    ID: { Input: string; Output: number | string }
-   JourneyStatus: { Input: string; Output: string }
  }

Consider removing the scalar definition since you're already defining this as an enum type in journey.ts.

📝 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.

  Scalars: {
    Date: { Input: Date; Output: Date }
    DateTimeISO: { Input: Date; Output: Date }
    DateTime: { Input: Date; Output: Date }
    ID: { Input: string; Output: number | string }
  }
🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/builder.ts around lines 67 to 70, the
JourneyStatus is defined as a scalar type, which conflicts with its enum
definition in journey.ts. To fix this, remove the JourneyStatus scalar type
definition from builder.ts to avoid schema inconsistencies and rely solely on
the enum type defined in journey.ts.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getCardBlocksContent.ts (1)

39-41: 💡 Verification agent

🧩 Analysis chain

Investigate redundant check

The condition block.id === cardBlock.id seems unnecessary since cardBlocksChildren[i] should only contain children of the card, not the card itself. This could indicate a misunderstanding of the data structure or a defensive check for unexpected data.


🏁 Script executed:

#!/bin/bash
# Check if cards can appear as their own children

echo "Checking for potential circular references in the block structure..."
fd "schema.prisma" -x cat {} | grep -A 30 "model Block"

echo "Checking for cases where a card might reference itself..."
rg "parentBlockId.*id" --type=ts --glob=!**/__generated__/**

Length of output: 99291


🏁 Script executed:

#!/bin/bash
# Display the contents of getCardBlocksContent.ts for review
sed -n '1,200p' apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getCardBlocksContent.ts

Length of output: 1447


Remove redundant self‐reference check

The block.id === cardBlock.id guard is unnecessary because cardBlocksChildren is built by filtering blocks on parentBlockId === cardBlock.id. A block cannot appear in its own children list unless the data is malformed. You can safely remove these lines.

• File: apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getCardBlocksContent.ts
• Remove at lines 39–41

Diff:

       for (let j = 0; j < orderedBlocks.length; j++) {
         const block = orderedBlocks[j]
-        if (block.id === cardBlock.id) {
-          continue
-        }
         cardResult += await getBlockContent({
           blocks,
           block
         })
       }
📝 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.

       for (let j = 0; j < orderedBlocks.length; j++) {
         const block = orderedBlocks[j]
         cardResult += await getBlockContent({
           blocks,
           block
         })
       }
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyAiTranslate/getCardBlocksContent/getCardBlocksContent.ts
at lines 39 to 41, remove the redundant check for block.id === cardBlock.id
since cardBlocksChildren is already filtered to only include children of
cardBlock, making this self-reference check unnecessary. Simply delete the if
condition and continue statement to clean up the code.
apis/api-journeys-modern/src/schema/journey/journey.ts (1)

39-47: 🛠️ Refactor suggestion

Add error handling to reference resolver

The resolveReference function doesn't handle the case where a journey with the given ID doesn't exist. Consider adding error handling or returning null explicitly.

  resolveReference: async (ref) => {
-    return prisma.journey.findUnique({
+    const journey = await prisma.journey.findUnique({
      where: { id: ref.id }
    })
+    if (!journey) {
+      console.warn(`Journey with ID ${ref.id} not found during reference resolution`)
+    }
+    return journey
  }
📝 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.

// Register as a federated entity
builder.asEntity(JourneyRef, {
  key: builder.selection<{ id: string }>('id'),
  resolveReference: async (ref) => {
    const journey = await prisma.journey.findUnique({
      where: { id: ref.id }
    })
    if (!journey) {
      console.warn(`Journey with ID ${ref.id} not found during reference resolution`)
    }
    return journey
  }
})
🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journey/journey.ts around lines 39 to 47,
the resolveReference function lacks error handling for cases when no journey is
found for the given ID. Modify the function to explicitly check if the result of
prisma.journey.findUnique is null or undefined, and handle this case by either
returning null or throwing a descriptive error. This ensures the resolver
behaves predictably when the referenced journey does not exist.
apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts (1)

24-27: 💡 Verification agent

🧩 Analysis chain

Avoid hard-coding the “English” locale – make it configurable

'529' is apparently the language-id for English, but by hard-coding it you force all callers to fetch the English translation of the language name.
If tomorrow we need the UI to run in another locale (e.g. Spanish) the code must be patched and redeployed.

-export async function getLanguageName(
-  apollo: ApolloClient<NormalizedCacheObject>,
-  languageId: string,
-  logger?: Logger
-): Promise<string | null> {
+// `localizedToLanguageId` defaults to English but can be overridden.
+export async function getLanguageName(
+  apollo: ApolloClient<NormalizedCacheObject>,
+  languageId: string,
+  logger?: Logger,
+  localizedToLanguageId = '529'
+): Promise<string | null> {
 ...
-        languageId: '529'
+        languageId: localizedToLanguageId
 ...
 }

This keeps the current behaviour (callers can omit the 4th arg) while unlocking multi-locale usage.
Don’t forget to update the call-sites.


I’d like to confirm where and how many places getLanguageName is invoked before finalizing the recommendation. Let’s search for the hard-coded locale and all call-sites:


🏁 Script executed:

#!/bin/bash
set -e

# Show the hard-coded '529' in the function
rg -n "languageId: '529'" -C2 apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts

# Find every place getLanguageName(...) is called
rg -n "getLanguageName\s*\(" -C1 .

Length of output: 580


Avoid hard-coding English locale – make it configurable

The hard-coded '529' forces every caller of getLanguageName to fetch English translations only. To support other locales without redeploying this function, add a fourth parameter with a default of '529':

--- a/apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts
+++ b/apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts
@@
-export async function getLanguageName(
-  apollo: ApolloClient<NormalizedCacheObject>,
-  languageId: string,
-  logger?: Logger
-): Promise<string | null> {
+// `localizedToLanguageId` defaults to English but can be overridden for other locales.
+export async function getLanguageName(
+  apollo: ApolloClient<NormalizedCacheObject>,
+  languageId: string,
+  logger?: Logger,
+  localizedToLanguageId = '529'
+): Promise<string | null> {
   const result = await apollo.query<{ language: { name: string } }>({
@@
       variables: {
         id: languageId,
-        languageId: '529'
+        languageId: localizedToLanguageId
       }
     })

• No existing call-sites are found in this repo, so you can merge this change now.
• When you introduce real calls to getLanguageName (e.g., from UI code), pass a different localizedToLanguageId to fetch names in other languages.

🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journeyAiTranslate/getLanguageName.ts
around lines 24 to 27, the languageId '529' is hard-coded to fetch English
translations only. Modify the function to accept a fourth parameter for the
locale languageId with a default value of '529'. Replace the hard-coded '529' in
the variables object with this parameter. Update all call-sites to optionally
pass this parameter to support fetching language names in different locales
without redeploying.
libs/journeys/ui/src/libs/useJourneyAiTranslateMutation/useJourneyAiTranslateMutation.ts (1)

44-70: 🛠️ Refactor suggestion

options.update silently overrides the built-in cache update

Because the spread operator comes after the custom update, any caller-supplied update prop will completely replace the cache write-back logic that appends the translated journey. This is easy to miss and will lead to missing items in the UI.

-  >(JOURNEY_AI_TRANSLATE_CREATE, {
-    update(cache, { data }) {
-
-    },
-    ...options
-  })
+  >(JOURNEY_AI_TRANSLATE_CREATE, {
+    ...options,
+    update(cache, result) {
+      // 1️⃣ first run the built-in update
+      if (result.data?.journeyAiTranslateCreate != null) {
+        cache.modify({ … })
+      }
+      // 2️⃣ then allow the caller to extend/override behaviour
+      options?.update?.(cache, result)
+    }
+  })

This preserves the default behaviour while still giving callers full control.

📝 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.

  >(JOURNEY_AI_TRANSLATE_CREATE, {
    ...options,
    update(cache, result) {
      // 1️⃣ first run the built-in update
      if (result.data?.journeyAiTranslateCreate != null) {
        cache.modify({
          fields: {
            adminJourneys(existingAdminJourneyRefs = []) {
              const translatedJourneyRef = cache.writeFragment({
                data: result.data.journeyAiTranslateCreate,
                fragment: gql`
                  fragment TranslatedJourney on Journey {
                    id
                    title
                    description
                    languageId
                    createdAt
                    updatedAt
                  }
                `
              })
              return [...existingAdminJourneyRefs, translatedJourneyRef]
            }
          }
        })
      }
      // 2️⃣ then allow the caller to extend/override behaviour
      options?.update?.(cache, result)
    }
  })
🤖 Prompt for AI Agents
In
libs/journeys/ui/src/libs/useJourneyAiTranslateMutation/useJourneyAiTranslateMutation.ts
between lines 44 and 70, the current code spreads the caller-supplied options
after defining a custom update function, causing any provided update function to
override the built-in cache update logic. To fix this, merge the caller's update
function with the built-in update by calling the built-in update first and then
the caller's update within a new combined update function, ensuring the default
cache modification always runs while still allowing callers to customize the
update behavior.
apis/api-journeys-modern/src/schema/journeyLanguageAiDetect/journeyLanguageAiDetect.ts (2)

22-40: ⚠️ Potential issue

Enforce access control – currently any authenticated user can probe any journey

withAuth({ isAuthenticated: true }) only checks login status.
A user who is not on the journey’s team can still call this mutation and leak up to 5 block contents to the AI vendor.

Add an ability/ACL check similar to journeyAiTranslateCreate, e.g.

const ability = await abilityForUser(user)
if (!ability.can('read', journey)) {
  throw new ForbiddenError('You do not have access to this journey.')
}

Failing early also avoids sending private content to the AI provider.

🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyLanguageAiDetect/journeyLanguageAiDetect.ts
around lines 22 to 40, the code lacks an access control check to verify if the
authenticated user has permission to read the journey, allowing unauthorized
users to access sensitive block content. To fix this, after fetching the
journey, obtain the user's ability object using abilityForUser(user) and check
if the user can read the journey with ability.can('read', journey). If the check
fails, throw a ForbiddenError to prevent unauthorized access and avoid sending
private content to the AI provider.

12-18: 🛠️ Refactor suggestion

Remove unused name field from the mutation input

input.name is never referenced in the resolver. Keeping unused required fields:

• inflates the client payload
• breaks backward compatibility if we later drop it
• misleads consumers about its purpose

-  name: t.input.string({ required: true }),

If the value will be needed in future, keep it but mark as optional for now.

📝 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.

      input: {
        journeyId: t.input.id({ required: true }),
        journeyLanguageName: t.input.string({ required: true }),
        textLanguageId: t.input.id({ required: true }),
        textLanguageName: t.input.string({ required: true })
      },
🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyLanguageAiDetect/journeyLanguageAiDetect.ts
between lines 12 and 18, the input field 'name' is defined as required but is
never used in the resolver. To fix this, remove the 'name' field entirely from
the input object if it is not needed, or alternatively, change it to be optional
by removing the 'required: true' flag if it might be used in the future. This
will prevent unnecessary payload inflation and avoid misleading API consumers.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx (1)

292-330: 🛠️ Refactor suggestion

Simplify dialog button selection in tests

The current approach to finding and clicking the right button in the dialog is complex with multiple fallback strategies, suggesting potential unpredictability in the component under test.

Consider adding a specific data-testid to the primary action button in the CopyToTeamDialog component to make testing more straightforward:

// In the CopyToTeamDialog component:
<Button 
  variant="contained" 
  onClick={handleCopy}
+ data-testid="copy-to-team-action-button"
>
  {t('Copy')}
</Button>

// Then in the test:
const copyButton = within(getByTestId('CopyToTeamDialog')).getByTestId('copy-to-team-action-button')
fireEvent.click(copyButton)

This would replace the entire complex button finding logic.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DuplicateJourneyMenuItem/DuplicateJourneyMenuItem.spec.tsx
around lines 292 to 330, the test uses a complex multi-step approach to find and
click the primary action button in the CopyToTeamDialog, which is fragile and
hard to maintain. To fix this, update the CopyToTeamDialog component to add a
unique data-testid attribute to the primary action button, then simplify the
test by directly querying and clicking that button using this data-testid,
removing all fallback logic and console logs.
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.tsx (1)

93-106: 🛠️ Refactor suggestion

Add error handling to translation process

The handleTranslate function doesn't handle errors that might occur during the translation process, potentially leaving users without feedback if something goes wrong.

Add error handling to provide feedback to users:

const handleTranslate = async (): Promise<void> => {
  if (
    selectedLanguage == null ||
    journeyData == null ||
    activeTeam?.id == null
  )
    return

+  try {
    await duplicateAndTranslate({
      teamId: activeTeam.id,
      selectedLanguage,
      shouldTranslate: true
    })
+  } catch (error) {
+    // Consider adding a notification or alert mechanism
+    console.error('Translation failed:', error)
+  }
}
📝 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.

  const handleTranslate = async (): Promise<void> => {
    if (
      selectedLanguage == null ||
      journeyData == null ||
      activeTeam?.id == null
    )
      return

    try {
      await duplicateAndTranslate({
        teamId: activeTeam.id,
        selectedLanguage,
        shouldTranslate: true
      })
    } catch (error) {
      // Consider adding a notification or alert mechanism
      console.error('Translation failed:', error)
    }
  }
🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.tsx
around lines 93 to 106, the handleTranslate function lacks error handling for
the asynchronous duplicateAndTranslate call. Wrap the await
duplicateAndTranslate call in a try-catch block to catch any errors, and in the
catch block, provide user feedback such as displaying an error message or
notification to inform the user that the translation process failed.
apis/api-journeys-modern/src/schema/journeyEventsExportLog/journeyEventsExportLog.spec.ts (1)

104-104: 🛠️ Refactor suggestion

Avoid banned {} generic – use generated default args instead

The static-analysis hint is correct: Prisma.JourneyGetPayload<{}> widens the type to “any non-nullish value”.
Prefer the Prisma-generated default type so IntelliSense and compile-time checks still work:

-type Journey = Prisma.JourneyGetPayload<{}>
+type Journey = Prisma.JourneyGetPayload<Prisma.JourneyDefaultArgs>

This keeps the payload definition precise and removes the lint error.

📝 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.

-type Journey = Prisma.JourneyGetPayload<{}>
+type Journey = Prisma.JourneyGetPayload<Prisma.JourneyDefaultArgs>
🧰 Tools
🪛 Biome (1.9.4)

[error] 104-104: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

🤖 Prompt for AI Agents
In
apis/api-journeys-modern/src/schema/journeyEventsExportLog/journeyEventsExportLog.spec.ts
at line 104, replace the use of the banned empty object generic `{}` in
`Prisma.JourneyGetPayload<{}>` with the Prisma-generated default argument type
by omitting the generic parameter entirely. This will ensure the type remains
precise, enable proper IntelliSense and compile-time checks, and resolve the
lint error.
apps/journeys-admin/src/components/Team/CopyToTeamMenuItem/CopyToTeamMenuItem.tsx (1)

53-65: ⚠️ Potential issue

Early-return guard erroneously blocks duplicated journeys when id prop is omitted

handleDuplicateJourney exits if id is null, yet id is unrelated to duplication (the mutation already knows the journey via journeyData.id).
When this component is used without the optional id prop, duplication silently fails.

-  if (id == null || journeyData == null) return
+  if (journeyData == null) return

You can also remove id from CopyToTeamMenuItemProps entirely if it’s no longer used elsewhere.

📝 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.

  const handleDuplicateJourney = async (
    teamId: string,
    selectedLanguage?: JourneyLanguage,
    showTranslation?: boolean
  ): Promise<void> => {
    if (journeyData == null) return

    await duplicateAndTranslate({
      teamId,
      selectedLanguage,
      shouldTranslate: showTranslation
    })
  }
🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/Team/CopyToTeamMenuItem/CopyToTeamMenuItem.tsx
around lines 53 to 65, the early return in handleDuplicateJourney incorrectly
checks if id is null and blocks duplication, but id is not needed for
duplication since journeyData.id is used. Remove the id null check from the
guard to allow duplication to proceed even if id is omitted. Also, remove id
from the CopyToTeamMenuItemProps if it is not used elsewhere in the component.
libs/journeys/ui/src/libs/useJourneyDuplicateAndTranslate/useJourneyDuplicateAndTranslate.ts (1)

52-72: ⚠️ Potential issue

Add defensive try/catch to guarantee loading is cleared and users are notified on network/APOLLO errors

await journeyDuplicate will throw on network, GraphQL, or auth errors.
Because the call isn’t wrapped in a try/catch, the promise rejection will bubble up, the hook will exit early, loading will remain true, and the user will never see feedback.

-    setLoading(true)
-    const { data: duplicateData } = await journeyDuplicate({
-      variables: { id: journeyId, teamId }
-    })
-
-    if (!duplicateData?.journeyDuplicate?.id) {
-      setLoading(false)
-      enqueueSnackbar(t('Journey duplication failed'), {
-        variant: 'error',
-        preventDuplicate: true
-      })
-      onError?.()
-      return
-    }
+    setLoading(true)
+    let duplicateData
+    try {
+      ;({ data: duplicateData } = await journeyDuplicate({
+        variables: { id: journeyId, teamId }
+      }))
+      if (!duplicateData?.journeyDuplicate?.id) {
+        throw new Error('Journey duplication failed')
+      }
+    } catch (err: unknown) {
+      enqueueSnackbar(
+        err instanceof Error ? err.message : t('Journey duplication failed'),
+        { variant: 'error', preventDuplicate: true }
+      )
+      onError?.()
+      setLoading(false)
+      return
+    }

This also removes the duplication of setLoading(false) and keeps the UX consistent in every failure path.

📝 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.

  const duplicateAndTranslate = async ({
    teamId,
    selectedLanguage,
    shouldTranslate
  }: DuplicateAndTranslateProps): Promise<string | undefined> => {
    if (journeyId == null) return

    setLoading(true)
    let duplicateData
    try {
      ;({ data: duplicateData } = await journeyDuplicate({
        variables: { id: journeyId, teamId }
      }))
      if (!duplicateData?.journeyDuplicate?.id) {
        throw new Error('Journey duplication failed')
      }
    } catch (err: unknown) {
      enqueueSnackbar(
        err instanceof Error ? err.message : t('Journey duplication failed'),
        { variant: 'error', preventDuplicate: true }
      )
      onError?.()
      setLoading(false)
      return
    }

    // …rest of your successful‐duplication/translatation flow…
🤖 Prompt for AI Agents
In
libs/journeys/ui/src/libs/useJourneyDuplicateAndTranslate/useJourneyDuplicateAndTranslate.ts
around lines 52 to 72, the async call to journeyDuplicate is not wrapped in a
try/catch block, causing loading state to remain true and no user feedback on
errors like network or GraphQL failures. Wrap the entire duplicateAndTranslate
function logic inside a try block, move setLoading(true) before the try, and in
a finally block call setLoading(false) to ensure loading is cleared. In the
catch block, enqueue an error snackbar and call onError if provided to notify
users of the failure.
apis/api-journeys-modern/src/schema/journey/journey.acl.spec.ts (1)

185-191: ⚠️ Potential issue

Test expectation contradicts its description – probable copy-paste error

The test is named “denies when user is publisher but has no userTeam or userJourneys” yet it asserts toBe(true):

expect(can(Action.Manage, templateJourney, publisherUser)).toBe(true)

If the intended behaviour is to deny, the expectation should be false; if the behaviour is to allow, rename the test to avoid confusion.

- expect(can(Action.Manage, templateJourney, publisherUser)).toBe(true)
+ expect(can(Action.Manage, templateJourney, publisherUser)).toBe(false)

A mismatch between the test name and assertion makes future maintenance error-prone and may hide real regressions.

🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journey/journey.acl.spec.ts around lines
185 to 191, the test name states it should deny access when the user is a
publisher without userTeam or userJourneys, but the assertion expects true. To
fix this, either change the assertion to expect false to match the deny behavior
described, or rename the test to reflect that access is allowed if the current
behavior is correct. Ensure the test name and expectation are consistent to
prevent confusion.
libs/journeys/ui/src/components/CopyToTeamDialog/CopyToTeamDialog.tsx (1)

71-90: 🛠️ Refactor suggestion

handleSubmit continues optimistic UI updates even when submitAction fails

submitAction can throw (e.g. network error during copy/translate).
The current implementation updates the active team, pushes UPDATE_LAST_ACTIVE_TEAM_ID, resets the form and closes the dialog regardless of success.

Wrap the call in try/catch and short-circuit follow-up mutations on failure:

-  async function handleSubmit(
+  async function handleSubmit(
     values: FormValues,
     { resetForm }: FormikHelpers<FormValues>
   ): Promise<void> {
-    await submitAction(
+    try {
+      await submitAction(
       values.teamSelect,
       values.languageSelect,
       values.showTranslation
     )
-
-    setActiveTeam(teams.find((team) => team.id === values.teamSelect) ?? null)
-    void updateLastActiveTeamId({
+      setActiveTeam(
+        teams.find((team) => team.id === values.teamSelect) ?? null
+      )
+      void updateLastActiveTeamId({
         variables: {
           input: {
             lastActiveTeamId: values.teamSelect
           }
         },
         onCompleted() {
           void client.refetchQueries({ include: ['GetAdminJourneys'] })
         }
-      },
-      onCompleted() {
-        void client.refetchQueries({ include: ['GetAdminJourneys'] })
-      }
-    })
-    resetForm()
-    onClose()
+      })
+      resetForm()
+      onClose()
+    } catch (err) {
+      /* Snackbar already shown upstream */
+    }
   }

This prevents incorrect UI state (team switch & dialog close) when the underlying operation fails.

📝 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.

  async function handleSubmit(
    values: FormValues,
    { resetForm }: FormikHelpers<FormValues>
  ): Promise<void> {
    try {
      await submitAction(
        values.teamSelect,
        values.languageSelect,
        values.showTranslation
      )

      setActiveTeam(
        teams.find((team) => team.id === values.teamSelect) ?? null
      )
      void updateLastActiveTeamId({
        variables: {
          input: {
            lastActiveTeamId: values.teamSelect
          }
        },
        onCompleted() {
          void client.refetchQueries({ include: ['GetAdminJourneys'] })
        }
      })
      resetForm()
      onClose()
    } catch (err) {
      /* Snackbar already shown upstream */
    }
  }
🤖 Prompt for AI Agents
In libs/journeys/ui/src/components/CopyToTeamDialog/CopyToTeamDialog.tsx around
lines 71 to 90, the handleSubmit function performs optimistic UI updates and
subsequent mutations even if submitAction throws an error. To fix this, wrap the
await submitAction call in a try/catch block and only proceed with updating the
active team, calling updateLastActiveTeamId, resetting the form, and closing the
dialog if submitAction succeeds. If an error occurs, catch it and prevent
further UI updates to avoid inconsistent state.
apis/api-journeys-modern/src/schema/journey/journey.acl.ts (2)

86-90: 🛠️ Refactor suggestion

⚠️ Potential issue

template existence check blocks all non-publisher Manage actions

if (action === Action.Manage && 'template' in journey) { return false }

The 'template' in journey guard is always true because the field exists on every Journey, even when template === false.
As a result, non-publisher owners or team managers can never manage any journey.

Replace the property-existence check with a value check:

-if (action === Action.Manage && 'template' in journey) {
+if (action === Action.Manage && journey.template === true) {

and let the switch-case below delegate to the manage helper for non-template journeys.

📝 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.

  // Non-publishers cannot manage templates
  if (action === Action.Manage && journey.template === true) {
    return false
  }
🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journey/journey.acl.ts around lines 86 to
90, the current check uses 'template' in journey which always returns true since
the property exists on every Journey object, blocking all non-publisher Manage
actions incorrectly. Replace this existence check with a value check that
verifies if journey.template is truthy, so only template journeys are blocked
for non-publishers. This allows the switch-case below to handle non-template
journeys properly via the manage helper.

26-33: ⚠️ Potential issue

INCLUDE_JOURNEY_ACL uses a non-existent Prisma helper type

Prisma.BlockInclude<DefaultArgs> does not exist in the generated client (only Prisma.<Model>Include).
This will fail type-checking and IDE autocomplete.

Consider exposing the concrete include type instead:

-export const INCLUDE_JOURNEY_ACL: Prisma.BlockInclude<DefaultArgs> = {
+export const INCLUDE_JOURNEY_ACL: Prisma.JourneyInclude = {

If the intention was to create a generic helper, move it to a locally-defined type instead of referencing an unknown Prisma helper.

📝 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.

export const INCLUDE_JOURNEY_ACL: Prisma.JourneyInclude = {
  journey: {
    include: {
      team: { include: { userTeams: true } },
      userJourneys: true
    }
  }
}
🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journey/journey.acl.ts around lines 26 to
33, the type Prisma.BlockInclude<DefaultArgs> used for INCLUDE_JOURNEY_ACL does
not exist in the Prisma client and causes type errors. Replace this with the
concrete include type generated by Prisma for the Journey model, such as
Prisma.JourneyInclude, to ensure proper type-checking and IDE support. If a
generic helper type is needed, define it locally instead of referencing a
non-existent Prisma helper type.
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts (2)

271-337: 🛠️ Refactor suggestion

Database hot-loop: thousands of single update calls inside stream

await prisma.block.update is executed once per partial translation chunk per block, potentially yielding hundreds of separate round-trips.

Batch updates per card or collect them and run a single prisma.$transaction for better throughput:

// collect
const pending: Prisma.BlockUpdateArgs[] = []

...

pending.push({
  where: { id: cleanBlockId, journeyId: input.journeyId },
  data: sanitisedUpdates
})

...

// after the stream finishes
if (pending.length > 0) {
  await prisma.$transaction(
    pending.map((args) => prisma.block.update(args))
  )
}

This change will drastically reduce DB latency and cost.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts
between lines 271 and 337, the code performs an individual prisma.block.update
call for each block inside the streaming loop, causing many separate database
round-trips. To fix this, accumulate all update operations for the blocks in an
array during the stream processing, then after the loop completes, execute a
single prisma.$transaction call with all the update operations batched together.
This will reduce database latency and improve throughput by minimizing the
number of separate update calls.

217-247: ⚠️ Potential issue

LLM output is applied without schema validation – high risk of over-updating blocks

Inside the streaming loop every item.updates object is passed straight into prisma.block.update, allowing the model to write any mutable field (e.g., journeyId, parentBlockId, tracking fields, etc.).

Mitigate by whitelisting the permissible keys per block type before persistence:

 const allowedKeys: Record<string, string[]> = {
   TypographyBlock: ['content'],
   ButtonBlock: ['label'],
   RadioOptionBlock: ['label'],
   RadioQuestionBlock: ['label'],
   TextResponseBlock: ['label', 'placeholder']
 }

 const sanitisedUpdates = Object.fromEntries(
   Object.entries(item.updates).filter(([key]) =>
     allowedKeys[block.typename]?.includes(key)
   )
 )

 if (Object.keys(sanitisedUpdates).length > 0) {
   await prisma.block.update({ data: sanitisedUpdates, ... })
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts
between lines 217 and 247, the code applies LLM output updates directly to
prisma.block.update without validating the fields, risking unintended overwrites
of sensitive or immutable fields. To fix this, implement a whitelist of allowed
fields to update for each block type (e.g., "content" for TypographyBlock,
"label" for ButtonBlock, etc.) and filter the item.updates object accordingly
before passing it to prisma.block.update, ensuring only these permitted fields
are updated.
apis/api-gateway/schema.graphql (2)

4322-4328: 🛠️ Refactor suggestion

Rename MutationJourneyLanguageAiDetectInput for consistency

To mirror JourneyAiTranslateInput and avoid the redundant Mutation prefix, rename the input type:

- input MutationJourneyLanguageAiDetectInput @join__type(graph: API_JOURNEYS_MODERN) {
+ input JourneyLanguageAiDetectInput @join__type(graph: API_JOURNEYS_MODERN) {

And update the mutation signature:

- journeyLanguageAiDetect(input: MutationJourneyLanguageAiDetectInput!) : Boolean! @join__field(graph: API_JOURNEYS_MODERN)
+ journeyLanguageAiDetect(input: JourneyLanguageAiDetectInput!) : Boolean! @join__field(graph: API_JOURNEYS_MODERN)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apis/api-gateway/schema.graphql around lines 4322 to 4328, rename the input
type MutationJourneyLanguageAiDetectInput to JourneyLanguageAiDetectInput to
maintain naming consistency and remove the redundant Mutation prefix. Also,
update any mutation definitions or references that use this input type to
reflect the new name JourneyLanguageAiDetectInput.

539-563: 💡 Verification agent

🧩 Analysis chain

Mark Journey as an extension in modern graph

The modern journeys graph should extend the base Journey type, not redeclare it. Update the @join__type directive to include extension: true:

- type Journey @join__type(graph: API_JOURNEYS, key: "id")  @join__type(graph: API_JOURNEYS_MODERN, key: "id")  {
+ type Journey @join__type(graph: API_JOURNEYS, key: "id")  @join__type(graph: API_JOURNEYS_MODERN, key: "id", extension: true)  {

Also, if the modern graph provides or requires additional fields on Journey, add corresponding @join__field(graph: API_JOURNEYS_MODERN) directives per field.


🏁 Script executed:

#!/bin/bash
set -e
# List all @join__type directives in the schema
rg "@join__type" -n apis/api-gateway/schema.graphql

# Check if any @join__type already uses extension: true
rg "extension: true" -n apis/api-gateway/schema.graphql

Length of output: 29705


Mark Journey as an extension in the modern graph

The Journey type in the modern subgraph must be marked as an extension, just like other shared types (e.g., Video, Language).

• File: apis/api-gateway/schema.graphql
• Line: ~539

- type Journey @join__type(graph: API_JOURNEYS, key: "id")  @join__type(graph: API_JOURNEYS_MODERN, key: "id")  {
+ type Journey @join__type(graph: API_JOURNEYS, key: "id")  @join__type(graph: API_JOURNEYS_MODERN, key: "id", extension: true)  {

If the modern graph introduces new fields on Journey, ensure each is annotated with

@join__field(graph: API_JOURNEYS_MODERN)
🤖 Prompt for AI Agents
In apis/api-gateway/schema.graphql around lines 539 to 563, the Journey type is
declared for both API_JOURNEYS and API_JOURNEYS_MODERN graphs without marking
the modern graph declaration as an extension. Update the @join__type directive
for API_JOURNEYS_MODERN to include extension: true to indicate it extends the
base Journey type. Additionally, if the modern graph adds or requires any new
fields on Journey, annotate those fields with @join__field(graph:
API_JOURNEYS_MODERN).

@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 23, 2025 00:09 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys May 23, 2025 00:09 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - videos-admin May 23, 2025 00:10 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - watch May 23, 2025 00:10 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 23, 2025 02:26 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - videos-admin May 23, 2025 02:56 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 23, 2025 02:56 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys May 23, 2025 02:56 Inactive
@edmonday edmonday merged commit a50260f into feature/25-04-MA-feat-ai-translation May 23, 2025
17 of 21 checks passed
@edmonday edmonday deleted the edmondshen/nes-244-refactor-translate-journey-dialog branch May 23, 2025 03:17
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.

2 participants