Skip to content

feat(pipeline): add pipeline actions to 3-dot menu and right-click context menu (EVO-990)#51

Merged
dpaes merged 4 commits into
developfrom
fix/EVO-990
May 13, 2026
Merged

feat(pipeline): add pipeline actions to 3-dot menu and right-click context menu (EVO-990)#51
dpaes merged 4 commits into
developfrom
fix/EVO-990

Conversation

@marcelogorutuba
Copy link
Copy Markdown
Member

@marcelogorutuba marcelogorutuba commented May 8, 2026

Summary

  • Add Add to pipeline / Move pipeline / Remove from pipeline submenu to the ChatHeader 3-dot menu
  • Add the same pipeline submenu to the ChatSidebar right-click context menu
  • Remove redundant inline PipelineManagement form from ContactSidebar
  • Pipeline state is loaded dynamically via getPipelinesByConversation when menus open (fixes stale null data returned by the list API)
  • After each action, loadSpecificConversation is called so the badge/icon on the conversation row updates immediately — no F5 needed
  • i18n keys added for all 5 locales: en, pt, es, fr, it

Test plan

  • Open a conversation without a pipeline → 3-dot menu shows Add to pipeline submenu; pick a stage → conversation badge updates immediately
  • Open the same conversation again → 3-dot menu now shows Move pipeline with a ✓ on the current stage; pick a different stage → badge updates
  • Right-click a conversation in the sidebar → same submenu behaviour as above
  • Remove from pipeline via right-click or 3-dot menu → badge disappears immediately
  • Verify no 400 Bad Request in network tab when moving between stages
  • Confirm ContactSidebar no longer renders the inline pipeline form

Closes EVO-990

🤖 Generated with Claude Code

Summary by Sourcery

Add pipeline management actions to chat conversation menus and streamline how pipeline state is loaded and displayed.

New Features:

  • Expose add/move/remove pipeline actions in the chat header three-dot menu for the active conversation.
  • Expose the same pipeline pipeline actions in the chat sidebar right-click context menu for conversations.

Enhancements:

  • Load conversation pipeline state on demand via pipeline APIs to avoid stale or null pipeline data in menus.
  • Refresh the affected conversation after pipeline changes so pipeline badges and icons update immediately in the UI.
  • Simplify the ContactSidebar by removing the redundant inline pipeline management form and relying on centralized pipeline controls.

Documentation:

  • Add i18n strings for the new pipeline actions across all supported locales.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 8, 2026

Reviewer's Guide

Implements pipeline management actions (add/move/remove) for conversations directly from the ChatHeader 3-dot menu and ChatSidebar right-click context menu, replaces the inline pipeline form in ContactSidebar, and wires everything to load pipeline state on demand and refresh conversations after pipeline changes with full i18n support.

Sequence diagram for pipeline management via ChatHeader 3-dot menu

sequenceDiagram
  actor User
  participant ChatHeader
  participant pipelinesService
  participant conversations
  participant Backend

  User->>ChatHeader: Open_3_dot_menu
  ChatHeader->>pipelinesService: getPipelines()
  pipelinesService->>Backend: GET /pipelines
  Backend-->>pipelinesService: pipelines_list
  pipelinesService-->>ChatHeader: pipelines_list

  ChatHeader->>pipelinesService: getPipelinesByConversation(conversationId)
  pipelinesService->>Backend: GET /pipelines?conversation_id
  Backend-->>pipelinesService: pipelines_with_stages_items
  pipelinesService-->>ChatHeader: pipelines_with_stages_items
  ChatHeader->>ChatHeader: setConvPipelineData(current_pipeline_stage)

  User->>ChatHeader: Select_pipeline_stage_in_submenu
  alt Conversation_already_in_pipeline_and_same_pipeline
    ChatHeader->>pipelinesService: moveItem(item_id,pipeline_id,from_stage_id,to_stage_id)
    pipelinesService->>Backend: POST /pipelines/{pipeline_id}/move_item
    Backend-->>pipelinesService: move_success
    pipelinesService-->>ChatHeader: move_success
    ChatHeader->>ChatHeader: update_convPipelineData(stageId)
  else Conversation_in_different_or_no_pipeline
    ChatHeader->>pipelinesService: addItemToPipeline(pipeline_id,{item_id,type,pipeline_stage_id})
    pipelinesService->>Backend: POST /pipelines/{pipeline_id}/items
    Backend-->>pipelinesService: add_success
    pipelinesService-->>ChatHeader: add_success
    ChatHeader->>ChatHeader: setConvPipelineData(new_pipeline_stage)
  end
  ChatHeader->>conversations: loadSpecificConversation(conversationId)
  conversations->>Backend: GET /conversations/{conversation_id}
  Backend-->>conversations: conversation_with_updated_pipeline_badge
  conversations-->>User: Updated_conversation_row_badge

  User->>ChatHeader: Click_remove_from_pipeline
  ChatHeader->>pipelinesService: removeItemFromPipeline(pipeline_id,conversationId)
  pipelinesService->>Backend: DELETE /pipelines/{pipeline_id}/items/{conversation_id}
  Backend-->>pipelinesService: remove_success
  pipelinesService-->>ChatHeader: remove_success
  ChatHeader->>ChatHeader: setConvPipelineData(null)
  ChatHeader->>conversations: loadSpecificConversation(conversationId)
  conversations->>Backend: GET /conversations/{conversation_id}
  Backend-->>conversations: conversation_without_pipeline_badge
  conversations-->>User: Badge_removed_immediately
Loading

Sequence diagram for pipeline management via ChatSidebar context menu

sequenceDiagram
  actor User
  participant ChatSidebar
  participant pipelinesService
  participant conversations
  participant Backend

  User->>ChatSidebar: Right_click_conversation_row
  ChatSidebar->>ChatSidebar: Open_context_menu

  ChatSidebar->>ChatSidebar: loadPipelinesOnce()
  alt Pipelines_not_loaded_yet
    ChatSidebar->>pipelinesService: getPipelines()
    pipelinesService->>Backend: GET /pipelines
    Backend-->>pipelinesService: pipelines_list
    pipelinesService-->>ChatSidebar: pipelines_list
    ChatSidebar->>ChatSidebar: setAllPipelines(pipelines)
  else Pipelines_already_loaded
    ChatSidebar->>ChatSidebar: Skip_loading_pipelines
  end

  ChatSidebar->>ChatSidebar: loadConversationPipelineState(conversationId)
  alt Pipeline_state_not_cached
    ChatSidebar->>pipelinesService: getPipelinesByConversation(conversationId)
    pipelinesService->>Backend: GET /pipelines?conversation_id
    Backend-->>pipelinesService: pipelines_with_stages_items
    pipelinesService-->>ChatSidebar: pipelines_with_stages_items
    ChatSidebar->>ChatSidebar: setConvPipelineStates(conversationId,current_pipeline_or_null)
  else Pipeline_state_cached
    ChatSidebar->>ChatSidebar: Use_cached_convPipelineStates
  end

  User->>ChatSidebar: Select_pipeline_stage_in_submenu
  alt Conversation_already_in_pipeline_and_same_pipeline
    ChatSidebar->>pipelinesService: moveItem(item_id,pipeline_id,from_stage_id,to_stage_id)
    pipelinesService->>Backend: POST /pipelines/{pipeline_id}/move_item
    Backend-->>pipelinesService: move_success
    pipelinesService-->>ChatSidebar: move_success
    ChatSidebar->>ChatSidebar: update_convPipelineStates_stage(conversationId)
  else Conversation_in_different_or_no_pipeline
    ChatSidebar->>pipelinesService: addItemToPipeline(pipeline_id,{item_id,type,pipeline_stage_id})
    pipelinesService->>Backend: POST /pipelines/{pipeline_id}/items
    Backend-->>pipelinesService: add_success
    pipelinesService-->>ChatSidebar: add_success
    ChatSidebar->>ChatSidebar: setConvPipelineStates(conversationId,new_pipeline_stage)
  end
  ChatSidebar->>conversations: loadSpecificConversation(conversationId)
  conversations->>Backend: GET /conversations/{conversation_id}
  Backend-->>conversations: conversation_with_updated_pipeline_badge
  conversations-->>User: Updated_conversation_row_badge

  User->>ChatSidebar: Click_remove_from_pipeline
  ChatSidebar->>pipelinesService: removeItemFromPipeline(pipeline_id,conversationId)
  pipelinesService->>Backend: DELETE /pipelines/{pipeline_id}/items/{conversation_id}
  Backend-->>pipelinesService: remove_success
  pipelinesService-->>ChatSidebar: remove_success
  ChatSidebar->>ChatSidebar: setConvPipelineStates(conversationId,null)
  ChatSidebar->>conversations: loadSpecificConversation(conversationId)
  conversations->>Backend: GET /conversations/{conversation_id}
  Backend-->>conversations: conversation_without_pipeline_badge
  conversations-->>User: Badge_removed_immediately
Loading

Class diagram for updated chat components and pipeline state

classDiagram
  class ChatHeader {
    +Conversation conversation
    +number? unreadCount
    -boolean menuOpen
    -Pipeline[] allPipelines
    -boolean isLoadingPipelines
    -ConvPipelineData? convPipelineData
    +renderConversationStatusDropdown()
    +handlePipelineStageSelect(pipelineId,stageId)
    +handleRemoveFromPipeline()
  }

  class ChatSidebar {
    +Pipeline[] allPipelines
    +Map~string,ConvPipelineState~ convPipelineStates
    +Set~string~ loadingConvPipelineRef
    +loadPipelinesOnce()
    +loadConversationPipelineState(conversationId)
    +handlePipelineStageSelect(conversation,pipelineId,stageId)
    +handleRemoveFromPipeline(conversation)
  }

  class ContactSidebar {
    -boolean showPipeline
    +ConversationPipelineItem conversationPipelineItem
    -PipelineManagement pipelineManagement_removed
  }

  class Conversation {
    +string id
    +any status
    +any custom_attributes
    +Inbox? inbox
  }

  class Inbox {
    +string name
  }

  class Pipeline {
    +string id
    +string name
    +Stage[] stages
  }

  class Stage {
    +string id
    +string name
    +number position
    +PipelineItem[] items
  }

  class PipelineItem {
    +string id
  }

  class ConvPipelineData {
    +string pipelineId
    +string stageId
    +string itemId
  }

  class ConvPipelineState {
    +string pipelineId
    +string stageId
    +string itemId
  }

  class pipelinesService {
    +getPipelines()
    +getPipelinesByConversation(conversationId)
    +moveItem(params)
    +addItemToPipeline(pipelineId,params)
    +removeItemFromPipeline(pipelineId,conversationId)
  }

  class conversations {
    +loadSpecificConversation(conversationId)
  }

  ChatHeader --> Conversation
  ChatHeader --> ConvPipelineData
  ChatHeader ..> pipelinesService
  ChatHeader ..> conversations

  ChatSidebar --> ConvPipelineState
  ChatSidebar ..> pipelinesService
  ChatSidebar ..> conversations

  Pipeline --> Stage
  Stage --> PipelineItem

  ContactSidebar --> ConversationPipelineItem

  class ConversationPipelineItem {
    +string conversationId
    +Pipeline[] pipelines
    +boolean isLoadingPipelines
    +onPipelineUpdated()
  }
Loading

File-Level Changes

Change Details Files
Add pipeline submenu to ChatSidebar right-click context menu with per-conversation lazy-loaded pipeline state and actions.
  • Introduce state to cache all pipelines globally in the sidebar and track per-conversation pipeline membership and loading state.
  • Add helper callbacks to load all pipelines once and to fetch a conversation’s pipeline via getPipelinesByConversation when the context submenu opens.
  • Implement handlers to move a conversation between stages, add it to a pipeline, or remove it, updating the local cache, showing toasts, and calling loadSpecificConversation for UI refresh.
  • Extend the sidebar context menu to render a nested pipeline submenu per conversation, including stage selection with a current-stage checkmark and a remove-from-pipeline action.
src/components/chat/chat-sidebar/ChatSidebar.tsx
Add pipeline management section to ChatHeader 3-dot menu with dynamic pipeline state and actions.
  • Wire ChatHeader to chat context to access conversations.loadSpecificConversation.
  • Load all pipelines once via pipelinesService.getPipelines and track loading state for the header menu.
  • On menu open, fetch the conversation’s actual pipeline assignment via getPipelinesByConversation and store pipelineId/stageId/itemId in local state.
  • Provide handlers to move the existing pipeline item between stages, add the conversation to a pipeline (including cross-pipeline moves), and remove it from the pipeline, with success/error toasts and conversation reload.
  • Render a labeled pipeline section in the dropdown, including nested pipeline → stage submenus with current stage checkmark, plus a remove-from-pipeline item when applicable.
src/components/chat/chat-header/ChatHeader.tsx
Simplify ContactSidebar pipeline UI by removing the inline management form.
  • Remove the PipelineManagement form block from the contact sidebar while retaining the ConversationPipelineItem list view and existing pipeline loading logic.
src/components/chat/contact-sidebar/ContactSidebar.tsx
Add i18n strings for pipeline actions across supported locales.
  • Introduce new translation keys under chatHeader.actions.pipeline for labels, loading messages, and success/error toasts in all five locales (en, es, fr, it, pt).
src/i18n/locales/en/chat.json
src/i18n/locales/es/chat.json
src/i18n/locales/fr/chat.json
src/i18n/locales/it/chat.json
src/i18n/locales/pt/chat.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The pipeline menu logic (loading pipelines, determining current stage, handling add/move/remove, rendering nested menus) is now duplicated between ChatHeader and ChatSidebar; consider extracting this into a shared hook and/or menu component so behavior stays consistent and future changes are made in one place.
  • In ChatHeader, getPipelines() is triggered on mount via a useEffect keyed on allPipelines.length, which means pipelines for this conversation are fetched even if the 3‑dot menu is never opened; to better match the lazy-loading behavior you added elsewhere, consider tying this fetch to menuOpen instead.
  • The sidebar’s pipeline state uses a Map stored in React state and includes that Map in hook dependencies (loadConversationPipelineState, handlePipelineStageSelect, handleRemoveFromPipeline), which causes these callbacks to be recreated on every state change; consider storing a plain object or using a ref for the map so you can avoid Map in dependency arrays and reduce unnecessary re-renders.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The pipeline menu logic (loading pipelines, determining current stage, handling add/move/remove, rendering nested menus) is now duplicated between ChatHeader and ChatSidebar; consider extracting this into a shared hook and/or menu component so behavior stays consistent and future changes are made in one place.
- In ChatHeader, `getPipelines()` is triggered on mount via a `useEffect` keyed on `allPipelines.length`, which means pipelines for this conversation are fetched even if the 3‑dot menu is never opened; to better match the lazy-loading behavior you added elsewhere, consider tying this fetch to `menuOpen` instead.
- The sidebar’s pipeline state uses a `Map` stored in React state and includes that `Map` in hook dependencies (`loadConversationPipelineState`, `handlePipelineStageSelect`, `handleRemoveFromPipeline`), which causes these callbacks to be recreated on every state change; consider storing a plain object or using a ref for the map so you can avoid `Map` in dependency arrays and reduce unnecessary re-renders.

## Individual Comments

### Comment 1
<location path="src/components/chat/chat-header/ChatHeader.tsx" line_range="169" />
<code_context>
+              type: 'conversation',
+              pipeline_stage_id: stageId,
+            });
+            setConvPipelineData({ pipelineId, stageId, itemId: convPipelineData.itemId });
+            toast.success(t('chatHeader.actions.pipeline.addSuccess'));
+          }
</code_context>
<issue_to_address>
**issue (bug_risk):** Using the existing itemId when adding to a different pipeline can cause incorrect client state.

When `convPipelineData` exists but the user picks a new `pipelineId`, `addItemToPipeline` will create a new item, but this code still stores `convPipelineData.itemId`. That leaves the UI pointing at the old item instead of the new one. Consider updating client state from the API response (or a follow-up fetch) so the tracked `itemId` matches what was actually created.
</issue_to_address>

### Comment 2
<location path="src/components/chat/chat-sidebar/ChatSidebar.tsx" line_range="242-251" />
<code_context>
+            });
+            setConvPipelineData(prev => (prev ? { ...prev, stageId } : null));
+            toast.success(t('chatHeader.actions.pipeline.moveSuccess'));
+          } else {
+            // Different pipeline — add (backend handles dedup/upsert)
+            await pipelinesService.addItemToPipeline(pipelineId, {
+              item_id: String(conversation.id),
+              type: 'conversation',
+              pipeline_stage_id: stageId,
+            });
+            setConvPipelineData({ pipelineId, stageId, itemId: convPipelineData.itemId });
+            toast.success(t('chatHeader.actions.pipeline.addSuccess'));
+          }
+        } else {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Pipeline state is not updated locally when adding a conversation that was not in any pipeline.

In the `else` branch (when `convState` is falsy), we add the conversation to a pipeline but never update `convPipelineStates`. Since the only follow-up is `conversations.loadSpecificConversation`, `convPipelineStates` stays `undefined` until the submenu is reopened and `loadConversationPipelineState` runs. This causes a brief mismatch where the UI still shows the "add" state after adding. Please also set `{ pipelineId, stageId, itemId }` in `convPipelineStates` here to keep the UI state consistent immediately.

Suggested implementation:

```typescript
          } else {
            // Different pipeline — add (backend handles dedup/upsert)
            const addedItem = await pipelinesService.addItemToPipeline(pipelineId, {
              item_id: String(conversation.id),
              type: 'conversation',
              pipeline_stage_id: stageId,
            });

            const itemId = addedItem?.id ?? String(conversation.id);

            setConvPipelineData({ pipelineId, stageId, itemId });
            setConvPipelineStates(prev =>
              new Map(prev).set(conversation.id, { pipelineId, stageId, itemId }),
            );
            toast.success(t('chatHeader.actions.pipeline.addSuccess'));
          }
        } else {

```

- If `pipelinesService.addItemToPipeline` returns a different shape (e.g. `{ item: { id: ... } }`), adjust the `itemId` extraction accordingly:
  - Replace `const itemId = addedItem?.id ?? String(conversation.id);` with the correct path (for example, `const itemId = addedItem.item.id;`).
- Ensure `setConvPipelineStates` is defined via `useState` in this component (it already appears to be used elsewhere, so this is likely already in place).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/components/chat/chat-header/ChatHeader.tsx Outdated
Comment thread src/components/chat/chat-sidebar/ChatSidebar.tsx Outdated
- Add pipeline submenu to ChatHeader 3-dot dropdown and ChatSidebar context menu
- Fix C1: remove from old pipeline before adding to new pipeline
- Fix H1: addItemToPipeline uses conversation.id, not item.id
- Fix M1: moveItem/removeItemFromPipeline use pipeline item.id, not conversation.id
- Fix M2: separate toast keys for add vs move errors (pipeline.addError/moveError)
- Fix M3/M7: always reload conv pipeline state after actions
- Fix M4: delete dead PipelineManagement component and its spec
- Fix M5: loadError retry in pipeline submenu
- Fix M6: distinguish loading from empty pipeline state (isPipelinesLoaded flag)
- Fix L2: move pipeline i18n keys to root pipeline.* namespace in all 6 locales
- Fix L5: stale-response guard via monotonic fetchId counter ref
- Add ChatHeader.spec.tsx with 5 tests covering H1, M1, M2, C1 behaviors
- Remove duplicate PipelineManagement block from ContactSidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcelogorutuba and others added 3 commits May 12, 2026 19:24
… loading (M3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…EVO-990)

- Dispatch updateConversation after pipeline action so conversation list badge
  updates immediately without page reload (C1/AC4)
- Replace .find() with .filter() + Promise.all to remove ALL pipeline memberships
  before adding to new pipeline, not just the first one found (H1)
- Reset convPipelineData when conversation.id changes to prevent stale pipeline
  state leaking into the next conversation's menu (M2)
- Remove dead i18n keys contactSidebar.pipeline.* from all 6 locales (M4)
- Add tests for badge dispatch (C1), multi-pipeline removal (H1), and
  isLoadingConvPipelines guard in ChatHeader and ChatSidebar (M5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replicate refreshConversationBadge in ChatSidebar to dispatch
  updateConversation after pipeline actions so conversation list
  badge updates immediately via right-click (CRITICAL/AC4)
- Replace Promise.all with Promise.allSettled in cross-pipeline
  remove flow in both ChatHeader and ChatSidebar to prevent partial
  state when one remove fails before add (HIGH)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@dpaes dpaes left a comment

Choose a reason for hiding this comment

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

Aprovado. Bloqueadores do ciclo anterior (CRITICAL/AC4 right-click + HIGH partial-failure rollback) fechados em cde20d6. ACs 1–4 passam. Achados menores deste ciclo foram migrados pra follow-ups separados: EVO-1127 (MEDIUM: invalidate cache + tests do ChatSidebar), EVO-1128 (LOW: refreshConversationBadge bypassa pipelineFetchCountRef no header), EVO-1129 (LOW: tratar item.id ausente como reject no allSettled). Detalhes completos no card EVO-990.

Merge com squash + --delete-branch.

@dpaes dpaes merged commit 751e1b7 into develop May 13, 2026
1 check passed
@dpaes dpaes deleted the fix/EVO-990 branch May 13, 2026 18:26
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