feat(pipeline): add pipeline actions to 3-dot menu and right-click context menu (EVO-990)#51
Conversation
Reviewer's GuideImplements 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 menusequenceDiagram
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
Sequence diagram for pipeline management via ChatSidebar context menusequenceDiagram
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
Class diagram for updated chat components and pipeline stateclassDiagram
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()
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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 auseEffectkeyed onallPipelines.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 tomenuOpeninstead. - The sidebar’s pipeline state uses a
Mapstored in React state and includes thatMapin 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 avoidMapin 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- 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>
dd78275 to
c899956
Compare
… 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>
dpaes
left a comment
There was a problem hiding this comment.
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.
Summary
PipelineManagementform fromContactSidebargetPipelinesByConversationwhen menus open (fixes stalenulldata returned by the list API)loadSpecificConversationis called so the badge/icon on the conversation row updates immediately — no F5 neededen,pt,es,fr,itTest plan
ContactSidebarno longer renders the inline pipeline formCloses 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:
Enhancements:
Documentation: