feat(chat): bulk resolve conversations via checkbox selection (EVO-1011)#50
feat(chat): bulk resolve conversations via checkbox selection (EVO-1011)#50marcelogorutuba wants to merge 3 commits into
Conversation
Add multi-select checkboxes to the conversation list sidebar with a bulk action toolbar that allows resolving multiple conversations at once via POST /api/v1/bulk_actions. Sidebar width increased from w-80 to w-96 to accommodate the full action label. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewer's GuideImplements bulk conversation resolution in the chat sidebar by adding checkbox-based multi-selection, a bulk action toolbar, a bulkResolve API call, and wiring selection lifecycle to filters/view changes and conversation status updates, along with a small sidebar width and i18n update. Sequence diagram for bulk resolving selected conversationssequenceDiagram
actor Agent
participant ChatSidebar
participant Chat
participant conversationAPI
participant Backend
participant ChatContext
Agent->>ChatSidebar: Click checkbox on conversation row
ChatSidebar-->>Chat: onToggleSelect(displayId)
Chat->>Chat: Update selectedConversationIds state
Chat-->>ChatSidebar: Rerender with selectedConversationIds
ChatSidebar->>ChatSidebar: Show bulk action toolbar
Agent->>ChatSidebar: Click bulk resolve button
ChatSidebar-->>Chat: onBulkResolve()
Chat->>Chat: Validate selectedConversationIds not empty
Chat->>Chat: Set isBulkResolving true
Chat->>conversationAPI: bulkResolve(displayIds)
conversationAPI->>Backend: POST /bulk_actions { type: Conversation, ids, fields: { status: resolved } }
Backend-->>conversationAPI: 200 OK
conversationAPI-->>Chat: Promise resolved
Chat->>Chat: Show success toast
Chat->>Chat: setSelectedConversationIds(new Set())
Chat->>Chat: For each resolved conversation set status resolved
Chat-->>ChatContext: conversations.updateConversation(updatedConversation)
ChatContext->>ChatContext: Remove resolved from open filter via WebSocket handler
ChatContext-->>ChatSidebar: Updated conversations list
Chat->>Chat: Set isBulkResolving false
ChatSidebar->>ChatSidebar: Bulk toolbar disappears when selection empty
Updated class diagram for Chat, ChatSidebar, and conversationAPI bulk resolveclassDiagram
class Chat {
- selectedConversationIds: Set~string~
- isBulkResolving: boolean
+ handleApplyFilters(newFilters: BaseFilter[]): Promise~void~
+ handleClearFilters(): Promise~void~
+ handleToggleConversationSelection(displayId: string): void
+ handleClearSelection(): void
+ handleBulkResolve(): Promise~void~
}
class ChatSidebar {
+ selectedConversationIds: Set~string~
+ onToggleSelect(displayId: string): void
+ onClearSelection(): void
+ onBulkResolve(): Promise~void~
+ isBulkResolving: boolean
}
class conversationAPI {
+ bulkResolve(displayIds: string[]): Promise~void~
}
Chat --> ChatSidebar : passes selection and bulk handlers
Chat --> conversationAPI : uses bulkResolve
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
useEffectinChatSidebarthat clears selection onshowArchivedchanges suppresses the exhaustive-deps rule; consider moving that responsibility to the parent (e.g., reacting to the same flag inChat) or memoizingonClearSelectionand including it in the dependency array instead of disabling ESLint. - The bulk resolve success/error toasts in
Chatare hardcoded strings and not using the i18n system; it would be more consistent to move these messages into thechatlocale files and allow proper translation and pluralization. - The checkbox in the conversation row only stops click propagation and is wrapped in a
divwithout handling keyboard events; consider making the checkbox itself the interactive element (or ensuring the wrapper is focusable and handles keyboard) to improve accessibility and avoid relying solely on mouse clicks.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `useEffect` in `ChatSidebar` that clears selection on `showArchived` changes suppresses the exhaustive-deps rule; consider moving that responsibility to the parent (e.g., reacting to the same flag in `Chat`) or memoizing `onClearSelection` and including it in the dependency array instead of disabling ESLint.
- The bulk resolve success/error toasts in `Chat` are hardcoded strings and not using the i18n system; it would be more consistent to move these messages into the `chat` locale files and allow proper translation and pluralization.
- The checkbox in the conversation row only stops click propagation and is wrapped in a `div` without handling keyboard events; consider making the checkbox itself the interactive element (or ensuring the wrapper is focusable and handles keyboard) to improve accessibility and avoid relying solely on mouse clicks.
## Individual Comments
### Comment 1
<location path="src/components/chat/chat-sidebar/ChatSidebar.tsx" line_range="700-709" />
<code_context>
+ ? t('chatSidebar.conversation')
+ : t('chatSidebar.conversations')}
+ </span>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-7 w-7 p-0 cursor-pointer"
+ onClick={onClearSelection}
+ >
+ <X className="h-3.5 w-3.5" />
+ </Button>
+ </div>
+ <Button
+ size="sm"
+ className="h-7 w-full cursor-pointer"
+ onClick={onBulkResolve}
+ disabled={isBulkResolving}
+ >
+ <CheckCircle className="h-3.5 w-3.5 mr-1.5" />
</code_context>
<issue_to_address>
**suggestion:** Consider providing a visual loading state for the bulk resolve button when `isBulkResolving` is true.
Disabling the button prevents duplicate submissions but doesn’t indicate progress to the user. Consider showing a loading state (e.g., spinner, updated label, or `loading` variant) while `isBulkResolving` is true to make the action feel responsive, especially for longer operations.
Suggested implementation:
```typescript
<Button
size="sm"
className="h-7 w-full cursor-pointer"
onClick={onBulkResolve}
disabled={isBulkResolving}
aria-busy={isBulkResolving}
>
{isBulkResolving ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t('chatHeader.actions.markAsResolved')}
</>
) : (
<>
<CheckCircle className="h-3.5 w-3.5 mr-1.5" />
{t('chatHeader.actions.markAsResolved')}
</>
)}
</Button>
```
You’ll also need to import the `Loader2` icon at the top of the file alongside the other icons, for example:
- If you currently have:
`import { X, CheckCircle } from 'lucide-react'`
change it to:
`import { X, CheckCircle, Loader2 } from 'lucide-react'`
Adjust the exact import line to match how icons are imported elsewhere in this file.
</issue_to_address>
### Comment 2
<location path="src/pages/Customer/Chat/Chat.tsx" line_range="220-227" />
<code_context>
+ setIsBulkResolving(true);
+ try {
+ await conversationAPI.bulkResolve(displayIds);
+ toast.success(`${displayIds.length} conversa(s) resolvida(s)`);
+ setSelectedConversationIds(new Set());
+ for (const conv of resolvedConversations) {
+ conversations.updateConversation({ ...conv, status: 'resolved' });
+ }
+ } catch (error) {
+ console.error('Bulk resolve error:', error);
+ toast.error('Erro ao resolver conversas em lote');
+ } finally {
+ setIsBulkResolving(false);
</code_context>
<issue_to_address>
**suggestion:** Use i18n translations for the bulk resolve toast messages instead of hardcoded Portuguese strings.
These success and error toasts are hardcoded in Portuguese, which is inconsistent with the localized UI. Please move the strings into an appropriate i18n namespace (e.g. `chat`) and use `t(...)` so they’re properly localized for all locales.
Suggested implementation:
```typescript
try {
await conversationAPI.bulkResolve(displayIds);
toast.success(t('bulkResolve.success', { count: displayIds.length }));
setSelectedConversationIds(new Set());
for (const conv of resolvedConversations) {
conversations.updateConversation({ ...conv, status: 'resolved' });
}
} catch (error) {
console.error('Bulk resolve error:', error);
toast.error(t('bulkResolve.error'));
} finally {
```
1. Ensure this component imports and initializes the i18n hook, for example:
- `import { useTranslation } from 'react-i18next';`
- Inside the component body: `const { t } = useTranslation('chat');`
2. Add the new translation keys to your i18n resource files under the `chat` namespace, for example:
- `bulkResolve.success`: something like `"{{count}} conversation(s) resolved"` with proper pluralization rules.
- `bulkResolve.error`: something like `"Error resolving conversations in bulk"`.
3. If your project uses a different namespace or translation hook pattern in this file, adapt the `useTranslation('chat')` call and the keys to match the existing conventions.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| className="h-7 w-7 p-0 cursor-pointer" | ||
| onClick={onClearSelection} | ||
| > | ||
| <X className="h-3.5 w-3.5" /> | ||
| </Button> | ||
| </div> | ||
| <Button |
There was a problem hiding this comment.
suggestion: Consider providing a visual loading state for the bulk resolve button when isBulkResolving is true.
Disabling the button prevents duplicate submissions but doesn’t indicate progress to the user. Consider showing a loading state (e.g., spinner, updated label, or loading variant) while isBulkResolving is true to make the action feel responsive, especially for longer operations.
Suggested implementation:
<Button
size="sm"
className="h-7 w-full cursor-pointer"
onClick={onBulkResolve}
disabled={isBulkResolving}
aria-busy={isBulkResolving}
>
{isBulkResolving ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t('chatHeader.actions.markAsResolved')}
</>
) : (
<>
<CheckCircle className="h-3.5 w-3.5 mr-1.5" />
{t('chatHeader.actions.markAsResolved')}
</>
)}
</Button>You’ll also need to import the Loader2 icon at the top of the file alongside the other icons, for example:
- If you currently have:
import { X, CheckCircle } from 'lucide-react'
change it to:
import { X, CheckCircle, Loader2 } from 'lucide-react'
Adjust the exact import line to match how icons are imported elsewhere in this file.
…1011)
- fix(i18n): replace hardcoded PT-BR toasts with t() calls in all 6 locales
- fix(bulk-resolve): consume success_ids/failed_ids for per-item failure surfacing
- fix(selection): clear selection when user opens a conversation row
- fix(permission): guard handleBulkResolve with can('conversations','update') check
- fix(context): use reloadCurrentFilters after bulk resolve instead of raw updateConversation
- fix(selection): cap bulk selection at 200 items client-side
- fix(i18n): wire chatSidebar.selected key into bulk action toolbar
- fix(lint): remove eslint-disable on onClearSelection useEffect dep
- fix(a11y): add aria-label to conversation row Checkbox
- fix(checkbox): use checked boolean in onCheckedChange handler
- fix(arch): move bulkResolve call from conversationAPI to chatService facade
- test: add vitest spec for chatService.bulkResolve and selection cap logic
- fix(layout): sidebar width expands to w-96 only when bulk toolbar is visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-1011) - fix(i18n): replace noPermissionSend with dedicated bulkResolveNoPermission key in all 6 locales - fix(i18n): replace broken plural concatenation with i18next native selectedCount_one/other keys - fix(dead-code): remove unused conversationAPI.bulkResolve from conversationService - fix(a11y): disable bulk resolve button when user lacks conversations:update permission Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
t()i18n calls in all 6 locales (bulkResolveSuccess,bulkResolvePartialSuccess,bulkResolveError)success_ids/failed_idsfrom backend response to show per-item failure toasthandleBulkResolvewithcan('conversations', 'update')permission checkreloadCurrentFilters()after bulk resolve instead of rawupdateConversation(fixes resolved conversations staying visible in open filter)chatSidebar.selectedi18n key into toolbar span (was dead)eslint-disable react-hooks/exhaustive-depsononClearSelectioneffect — add dep insteadaria-labelto conversation row<Checkbox>for screen readerscheckedboolean inonCheckedChangehandlerbulkResolvecall fromconversationAPIimport tochatServicefacadechatService.bulkResolve(3 cases) and selection cap logicw-80and only expands tow-96when bulk toolbar is visibleValidation
evo-ai-frontend-community: pnpm exec tsc -b --noEmit✅evo-ai-frontend-community: pnpm lint(0 errors in changed files) ✅evo-ai-frontend-community: npx vitest run src/services/chat/chatService.bulkResolve.spec.ts✅ 4/4 passedChanged Files
src/pages/Customer/Chat/Chat.tsxsrc/components/chat/chat-sidebar/ChatSidebar.tsxsrc/services/chat/chatService.tssrc/services/chat/chatService.bulkResolve.spec.tssrc/i18n/locales/en/chat.jsonsrc/i18n/locales/es/chat.jsonsrc/i18n/locales/fr/chat.jsonsrc/i18n/locales/it/chat.jsonsrc/i18n/locales/pt/chat.jsonsrc/i18n/locales/pt-BR/chat.jsonRelated PRs
Linked Issue