Skip to content

Instantly share code, notes, and snippets.

@diegows
Last active January 12, 2026 01:48
Show Gist options
  • Select an option

  • Save diegows/bf02b7c298a8397b38912fcbeff39bd9 to your computer and use it in GitHub Desktop.

Select an option

Save diegows/bf02b7c298a8397b38912fcbeff39bd9 to your computer and use it in GitHub Desktop.
Feature Plan: Sidebar Filters & Status Tabs Refactoring for Chatwoot

Implementation Plan: Conversation Sidebar Filters & Status Tabs Refactoring

Requirements Summary

Replace assignee-based tabs (Mine/Unassigned/All) with 5 sidebar filters and 2 status tabs:

New Sidebar Filters

  1. All Waiting: team_id IS NULL AND assignee_id IS NULL
  2. Team Waiting: team_id IN (user's teams) AND assignee_id IS NULL
  3. My Chats: assignee_id = current_user.id
  4. My Team Chats: assignee_id IN (team_member_ids) AND assignee_id != current_user.id
  5. All Chats: All other conversations (assigned to other teams/agents)

New Status Tabs

  • In Progress: status IN ('open', 'pending', 'snoozed')
  • Done: status = 'resolved'

Accept Feature

  • Button appears on conversation cards in "All Waiting" and "Team Waiting" filters
  • Also available in right-click context menu
  • Assigns conversation to current user via existing POST /assignments endpoint
  • Only visible when assignee_id IS NULL

User Decisions

  • ✅ Pending/snoozed conversations appear in "In Progress" tab
  • ✅ "All Chats" filter respects current status tab selection
  • ✅ Accept button appears BOTH on card AND in context menu
  • ✅ Team filtering uses ALL teams the current user belongs to

Implementation Overview

Frontend Changes

Replace tabs with sidebar filters → Update filter logic → Add Accept button → Modify store getters

Backend Changes

Extend ConversationFinder → Add new filter types → Return new counts → Handle team-based queries


Critical Files to Modify

Frontend (5 files)

  1. app/javascript/dashboard/components/ChatList.vue (Lines 271-343)

    • Change activeAssigneeTabactiveFilterType
    • Update conversationFilters computed: assigneeTypefilterType
    • Modify conversationList computed to handle 5 filter types
    • Add acceptConversation() function and provide it to children
    • Replace ChatTypeTabs component with new ConversationSidebarFilters
  2. app/javascript/dashboard/constants/globals.js

    • Add new constant:
    SIDEBAR_FILTER_TYPE: {
      ALL_WAITING: 'all_waiting',
      TEAM_WAITING: 'team_waiting',
      MY_CHATS: 'my_chats',
      MY_TEAM_CHATS: 'my_team_chats',
      ALL_CHATS: 'all_chats'
    }
  3. app/javascript/dashboard/store/modules/conversations/getters.js

    • Add 3 new getters:
      • getAllWaitingChats: Filter conversations with no team and no assignee
      • getTeamWaitingChats: Filter by user's team IDs where unassigned
      • getMyTeamChatsExcludingMine: Filter by user's teams where assigned to teammate
  4. app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue

    • Add showAcceptButton prop
    • Add Accept button in card UI (Tailwind styled)
    • Emit acceptConversation event
  5. app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue

    • Add Accept menu item
    • Show only when conversation is unassigned
    • Emit acceptConversation event

Backend (1 file)

  1. app/finders/conversation_finder.rb (Lines 109-175)
    • Extend filter_by_assignee_type() to handle new filter types:
    when 'all_waiting'
      @conversations = @conversations.unassigned.where(team_id: nil)
    when 'team_waiting'
      user_team_ids = current_user.teams.pluck(:id)
      @conversations = @conversations.unassigned.where(team_id: user_team_ids)
    when 'my_team_chats'
      user_team_ids = current_user.teams.pluck(:id)
      @conversations = @conversations
        .where(team_id: user_team_ids)
        .where.not(assignee_id: current_user.id)
        .where.not(assignee_id: nil)
    • Update filter_by_status() to accept array of statuses for "In Progress" tab
    • Modify set_count_for_all_conversations() to return 6 counts instead of 3
    • Update perform() return value to include new counts

New Components to Create

  1. app/javascript/dashboard/components/widgets/ConversationSidebarFilters.vue (NEW)

    • Renders vertical list of 5 filters with icons and counts
    • Emits filterChange(filterKey) on selection
    • Shows active filter state
  2. app/javascript/dashboard/components/widgets/ConversationStatusTabs.vue (NEW)

    • Renders 2 tabs: "In Progress" and "Done"
    • Similar to ChatTypeTabs but for status
    • Emits statusChange(statusKey)

Implementation Steps

Phase 1: Backend Foundation

  1. Update conversation_finder.rb

    • Add new filter types to filter_by_assignee_type()
    • Implement team member filtering logic
    • Modify filter_by_status() to accept status arrays
    • Update set_count_for_all_conversations() to return 6 counts
    • Update perform() method return hash
  2. Test backend changes

    • Run: bundle exec rspec spec/finders/conversation_finder_spec.rb
    • Add new specs for 'all_waiting', 'team_waiting', 'my_team_chats' filter types

Phase 2: Frontend Constants & Store

  1. Add filter constants

    • Update globals.js with SIDEBAR_FILTER_TYPE
  2. Add store getters

    • Add getAllWaitingChats, getTeamWaitingChats, getMyTeamChatsExcludingMine to getters.js
    • Each getter filters conversations client-side based on team membership and assignment

Phase 3: New Components

  1. Create ConversationSidebarFilters.vue

    • Vertical list layout with Tailwind classes
    • Show filter name, icon, and count
    • Highlight active filter
    • Emit filterChange event
  2. Create ConversationStatusTabs.vue

    • Two-tab layout (In Progress / Done)
    • Similar structure to existing ChatTypeTabs
    • Emit statusChange event

Phase 4: Update ChatList Component

  1. Modify ChatList.vue
    • Remove ChatTypeTabs import/usage
    • Import new ConversationSidebarFilters and ConversationStatusTabs
    • Rename activeAssigneeTabactiveFilterType
    • Update conversationFilters computed property
    • Update conversationList computed with switch statement for 5 filters
    • Add acceptConversation() function
    • Provide acceptConversation to child components

Phase 5: Accept Button UI

  1. Update ConversationCard.vue

    • Add showAcceptButton prop
    • Add Accept button in card footer area
    • Emit acceptConversation event with conversation ID
    • Style with Tailwind utilities
  2. Update contextMenu/Index.vue

    • Add Accept option to menu constants
    • Add Accept menu item in template
    • Show only when !chat.meta.assignee
    • Emit acceptConversation event
  3. Update ConversationItem.vue

    • Inject acceptConversation function
    • Pass showAcceptButton prop to ConversationCard
    • Calculate visibility based on active filter
    • Forward acceptConversation events

Phase 6: Translations

  1. Update i18n files
    • Add translations to app/javascript/dashboard/i18n/locale/en/chatlist.json
    • Add SIDEBAR_FILTERS translations
    • Add STATUS_TABS translations
    • Add Accept button labels

Phase 7: Testing

  1. Write frontend tests

    • Test new components (ConversationSidebarFilters, ConversationStatusTabs)
    • Update ChatList.spec.js for new filter logic
    • Test Accept button visibility logic
    • Test acceptConversation action
  2. Write backend tests

    • Update conversation_finder_spec.rb
    • Test new filter types
    • Test team-based filtering
    • Test count calculations

Data Flow

Filter Selection

User clicks filter → ConversationSidebarFilters emits filterChange
→ ChatList updates activeFilterType
→ conversationFilters computed recalculates
→ Store getters filter conversations
→ UI updates

Accept Action

User clicks Accept → Event bubbles to ChatList
→ acceptConversation(conversationId) called
→ Vuex action 'assignAgent' dispatched
→ POST /api/v1/accounts/{id}/conversations/{id}/assignments
→ Backend assigns conversation to user
→ Real-time update removes from current filter
→ Conversation appears in "My Chats"

Edge Cases Handled

  1. User not in any teams

    • "Team Waiting" and "My Team Chats" show empty state
    • Backend returns empty array for user_team_ids
  2. Conversation with team_id AND assignee_id

    • Appears in "My Chats" if assigned to current user (priority)
    • Appears in "My Team Chats" if assigned to teammate
    • Does NOT appear in "Team Waiting" (not unassigned)
  3. Multiple team membership

    • User sees conversations from ALL their teams
    • Query uses team_id IN (all_user_team_ids)
  4. Accept button visibility

    • Only show in "All Waiting" and "Team Waiting" filters
    • Only show when assignee_id IS NULL
    • Hide in other filters automatically

Verification Steps

Manual Testing

  1. Test sidebar filters

    - Log in as user with multiple team memberships
    - Click "All Waiting" → Should show conversations with no team, no assignee
    - Click "Team Waiting" → Should show team conversations with no assignee
    - Click "My Chats" → Should show conversations assigned to you
    - Click "My Team Chats" → Should show teammate conversations
    - Click "All Chats" → Should show remaining conversations
    
  2. Test status tabs

    - Click "In Progress" → Should show open, pending, snoozed conversations
    - Click "Done" → Should show resolved conversations
    - Switch between tabs in each filter
    
  3. Test Accept button

    - Navigate to "All Waiting" filter
    - Verify Accept button appears on unassigned conversations
    - Click Accept → Conversation should disappear from list
    - Navigate to "My Chats" → Should see accepted conversation
    - Test Accept from context menu (right-click)
    - Navigate to "Team Waiting" and repeat Accept test
    
  4. Test edge cases

    - Log in as user with no teams → Team filters should be empty
    - Test with conversations assigned to other teams
    - Test Accept race condition (two users clicking simultaneously)
    

Automated Testing

  1. Run test suites

    # Backend tests
    bundle exec rspec spec/finders/conversation_finder_spec.rb
    bundle exec rspec spec/controllers/api/v1/accounts/conversations_controller_spec.rb
    
    # Frontend tests
    pnpm test -- ConversationSidebarFilters.spec.js
    pnpm test -- ChatList.spec.js
    pnpm test -- ConversationCard.spec.js
  2. Run linters

    pnpm eslint
    bundle exec rubocop -a
  3. Integration test

    # Start dev environment
    pnpm dev
    
    # Open browser and verify:
    # - All filters work correctly
    # - Counts update in real-time
    # - Accept button assigns conversations
    # - Status tabs filter correctly

Backward Compatibility

  • Keep old assigneeType parameter support during transition
  • Map old values: 'me''my_chats', 'unassigned''all_waiting', 'all''all_chats'
  • Custom views use advanced filters (unaffected by this change)
  • No database migrations required

Rollback Strategy

If issues arise:

  1. No database changes → Rollback is safe
  2. Revert frontend to show old ChatTypeTabs component
  3. Backend remains backward compatible with old filter types
  4. Consider adding feature flag for gradual rollout

Enterprise Considerations

Before implementation:

  1. Check enterprise/app/finders/enterprise/conversation_finder.rb for overrides
  2. Test compatibility with Enterprise team hierarchies
  3. Update Enterprise specs to mirror OSS changes
  4. Verify SLA policies work with new filters

Performance Notes

  • New implementation adds 3 additional count queries
  • Client-side filtering uses cached Vuex getters (no performance impact expected)
  • Virtual scrolling already handles large conversation lists
  • Consider adding database index: conversations(team_id, assignee_id, status) if queries slow down

Translation Keys Needed

Add to app/javascript/dashboard/i18n/locale/en/chatlist.json:

{
  "CHAT_LIST": {
    "SIDEBAR_FILTERS": {
      "all_waiting": "All Waiting",
      "team_waiting": "Team Waiting",
      "my_chats": "My Chats",
      "my_team_chats": "My Team Chats",
      "all_chats": "All Chats"
    },
    "STATUS_TABS": {
      "in_progress": "In Progress",
      "done": "Done"
    }
  },
  "CONVERSATION": {
    "ACCEPT": "Accept",
    "ACCEPT_SUCCESS": "Conversation assigned to you",
    "ACCEPT_FAILED": "Failed to accept conversation",
    "CARD_CONTEXT_MENU": {
      "ACCEPT": "Accept conversation"
    }
  }
}

Summary

This refactoring modernizes the conversation sidebar by:

  • Replacing 3 assignee tabs with 5 granular filters
  • Adding dedicated status tabs for better workflow organization
  • Implementing one-click Accept functionality for faster conversation assignment
  • Improving team-based conversation management

The implementation maintains backward compatibility, requires no database migrations, and follows Chatwoot's existing patterns for minimal risk and maximum maintainability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment