Skip to content

feat(chat): bulk resolve conversations via checkbox selection (EVO-1011)#50

Open
marcelogorutuba wants to merge 3 commits into
developfrom
fix/EVO-1011
Open

feat(chat): bulk resolve conversations via checkbox selection (EVO-1011)#50
marcelogorutuba wants to merge 3 commits into
developfrom
fix/EVO-1011

Conversation

@marcelogorutuba
Copy link
Copy Markdown
Member

@marcelogorutuba marcelogorutuba commented May 8, 2026

Summary

  • Replace hardcoded PT-BR toasts with t() i18n calls in all 6 locales (bulkResolveSuccess, bulkResolvePartialSuccess, bulkResolveError)
  • Consume success_ids/failed_ids from backend response to show per-item failure toast
  • Clear checkbox selection when user opens a conversation row (AC feat(dashboard): add inline tooltips with tour copy to all dashboard … #5)
  • Guard handleBulkResolve with can('conversations', 'update') permission check
  • Use reloadCurrentFilters() after bulk resolve instead of raw updateConversation (fixes resolved conversations staying visible in open filter)
  • Cap bulk selection at 200 items client-side
  • Wire chatSidebar.selected i18n key into toolbar span (was dead)
  • Remove eslint-disable react-hooks/exhaustive-deps on onClearSelection effect — add dep instead
  • Add aria-label to conversation row <Checkbox> for screen readers
  • Use checked boolean in onCheckedChange handler
  • Move bulkResolve call from conversationAPI import to chatService facade
  • Add vitest spec for chatService.bulkResolve (3 cases) and selection cap logic
  • Sidebar width stays w-80 and only expands to w-96 when bulk toolbar is visible

Validation

  • 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 passed

Changed Files

  • src/pages/Customer/Chat/Chat.tsx
  • src/components/chat/chat-sidebar/ChatSidebar.tsx
  • src/services/chat/chatService.ts
  • src/services/chat/chatService.bulkResolve.spec.ts
  • 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
  • src/i18n/locales/pt-BR/chat.json

Related PRs

Linked Issue

  • EVO-1011

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>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 8, 2026

Reviewer's Guide

Implements 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 conversations

sequenceDiagram
  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
Loading

Updated class diagram for Chat, ChatSidebar, and conversationAPI bulk resolve

classDiagram

  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
Loading

File-Level Changes

Change Details Files
Add checkbox-based multi-select and bulk action toolbar to the chat sidebar list.
  • Extend ChatSidebar props with selection-related inputs and bulk resolve handlers
  • Render a checkbox for each conversation row, preventing row click propagation and styling it to stay visible across themes
  • Show a bulk action toolbar when there are selected conversations, displaying the count, a clear-selection button, and a primary button that triggers bulk resolve
  • Increase sidebar width from md:w-80 to md:w-96 to accommodate the toolbar label without overflow
src/components/chat/chat-sidebar/ChatSidebar.tsx
Manage selected conversations and bulk resolve behavior in the main Chat page.
  • Add React state for selected conversation IDs and bulk resolving flag
  • Clear selection whenever filters are applied or cleared
  • Implement toggle and clear handlers for selection, using a Set to track selected display_ids
  • Implement handleBulkResolve to call the conversation bulkResolve API with selected IDs, show success/error toasts, clear selection, and update conversation status to resolved in context
  • Pass selection and bulk resolve handlers and state down to ChatSidebar
src/pages/Customer/Chat/Chat.tsx
Expose a bulkResolve API method for conversations.
  • Add conversationAPI.bulkResolve that POSTs to /bulk_actions with type Conversation, the selected display_ids, and fields.status set to resolved
src/services/conversations/conversationService.ts
Update i18n resources to support selection-related labels for the new toolbar.
  • Add the selected key (and related strings like singular/plural conversation labels as needed) to chat.json for all supported locales (en, es, fr, it, pt-BR, pt) to localize the toolbar text
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-BR/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 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.
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>

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 on lines +700 to +709
<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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/pages/Customer/Chat/Chat.tsx Outdated
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant