From 665e531dc3c541363d82573a8eb86c2f2103dd0d Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 14 Sep 2026 17:30:24 +0800 Subject: [PATCH] fix: keep pending widget conversations visible and searchable --- backend/internal/app/bootstrap.go | 1 + backend/internal/service/widget_service.go | 21 +++++++++++++++++++ .../internal/service/widget_service_test.go | 4 ++++ .../dashboard/components/ChatList.vue | 3 ++- .../store/modules/conversations/getters.js | 9 ++++++-- .../store/modules/conversations/helpers.js | 7 ++++++- .../specs/conversations/getters.spec.js | 14 +++++++++++++ .../specs/conversations/helpers.spec.js | 19 +++++++++++++++++ 8 files changed, 74 insertions(+), 4 deletions(-) diff --git a/backend/internal/app/bootstrap.go b/backend/internal/app/bootstrap.go index dde96634..ad06881a 100644 --- a/backend/internal/app/bootstrap.go +++ b/backend/internal/app/bootstrap.go @@ -808,6 +808,7 @@ func Bootstrap(env string) (*App, error) { widgetOfflineMessageRepo := repository.NewWidgetOfflineMessageRepo(db) widgetService := service.NewWidgetService(inboxRepo, contactRepo, contactInboxRepo, conversationRepo, messageRepo, widgetTypingAdapter, widgetThemeConfigRepo, preChatFormRepo, widgetFileUploadRepo, widgetOfflineMessageRepo, inboxMemberRepo, tagRepo, campaignRepo) widgetService.SetWorkerPool(workerPool) + widgetService.SetSearchIndexer(searchIndexer) widgetService.SetDispatcher(channelDispatcher) widgetService.SetRealtimeEventBridge(realtimeBridge) widgetHandler := widget.NewHandler(widgetService) diff --git a/backend/internal/service/widget_service.go b/backend/internal/service/widget_service.go index d637abfc..9e748334 100644 --- a/backend/internal/service/widget_service.go +++ b/backend/internal/service/widget_service.go @@ -68,6 +68,7 @@ type WidgetService struct { worker *worker.WorkerPool dispatcher *channel.Dispatcher realtime *wsevent.BridgeListener + searchIndexer SearchIndexer } // NewWidgetService creates a new Widget service. @@ -120,6 +121,22 @@ func (s *WidgetService) SetRealtimeEventBridge(bridge *wsevent.BridgeListener) { s.realtime = bridge } +func (s *WidgetService) SetSearchIndexer(indexer SearchIndexer) { + s.searchIndexer = indexer +} + +func (s *WidgetService) indexConversation(ctx context.Context, conversation *model.Conversation) { + if s.searchIndexer != nil && conversation != nil { + logSearchIndexError("conversation", conversation.ID, s.searchIndexer.IndexConversation(ctx, conversation)) + } +} + +func (s *WidgetService) indexMessage(ctx context.Context, message *model.Message) { + if s.searchIndexer != nil && message != nil { + logSearchIndexError("message", message.ID, s.searchIndexer.IndexMessage(ctx, message)) + } +} + // --- DTOs --- // WidgetInitRequest is the DTO for the /widget/init endpoint. @@ -536,6 +553,8 @@ func (s *WidgetService) createIncomingMessage(ctx context.Context, conversation if s.realtime != nil { s.realtime.PublishEnqueued(ctx, realtimeJobs) } + s.indexMessage(ctx, message) + s.indexConversation(ctx, conversation) return attachments, nil } @@ -967,6 +986,7 @@ func (s *WidgetService) PublicCreateConversation(ctx context.Context, inboxIdent return nil, err } } + s.indexConversation(ctx, conversation) return conversation, nil } @@ -996,6 +1016,7 @@ func (s *WidgetService) PublicToggleStatus(ctx context.Context, inboxIdentifier, if err := s.conversationRepo.Update(ctx, conversation); err != nil { return nil, err } + s.indexConversation(ctx, conversation) return conversation, nil } diff --git a/backend/internal/service/widget_service_test.go b/backend/internal/service/widget_service_test.go index 1591beff..d78dfbfa 100644 --- a/backend/internal/service/widget_service_test.go +++ b/backend/internal/service/widget_service_test.go @@ -749,6 +749,8 @@ func TestWidgetService_SendMessage_AutomaticCaptainTakeover(t *testing.T) { require.NoError(t, db.Create(&model.AgentBotInbox{AgentBotID: bot.ID, InboxID: inbox.ID, Status: model.AgentBotInboxActive}).Error) require.NoError(t, db.Create(&model.CaptainPreference{AccountID: account.ID, AutoReplyEnabled: true}).Error) svc.SetWorkerPool(worker.NewWorkerPool(db)) + indexer := &mockServiceSearchIndexer{} + svc.SetSearchIndexer(indexer) initResp, err := svc.Init(context.Background(), WidgetInitRequest{WebsiteToken: "test_ws_token_123"}) require.NoError(t, err) @@ -761,6 +763,8 @@ func TestWidgetService_SendMessage_AutomaticCaptainTakeover(t *testing.T) { require.NotNil(t, conversation.AssigneeAgentBotID) assert.Equal(t, bot.ID, *conversation.AssigneeAgentBotID) assert.Equal(t, uint(1), conversation.AITakeoverVersion) + assert.Contains(t, indexer.indexedConversationIDs, response.ConversationID) + assert.Contains(t, indexer.indexed, "message") var job model.BackgroundJob require.NoError(t, db.Where("job_type = ?", TaskTypeCaptainConversationResponseBuilder).First(&job).Error) } diff --git a/frontend/app/javascript/dashboard/components/ChatList.vue b/frontend/app/javascript/dashboard/components/ChatList.vue index df735929..5d0abfaa 100644 --- a/frontend/app/javascript/dashboard/components/ChatList.vue +++ b/frontend/app/javascript/dashboard/components/ChatList.vue @@ -50,6 +50,7 @@ import { filterItemsByPermission, } from 'dashboard/helper/permissionsHelper.js'; import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers'; +import { isConversationUnassigned } from '../store/modules/conversations/helpers'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js'; @@ -305,7 +306,7 @@ function filterByAssigneeTab(conversations) { ); } if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.UNASSIGNED) { - return conversations.filter(c => !c.meta?.assignee); + return conversations.filter(isConversationUnassigned); } return [...conversations]; } diff --git a/frontend/app/javascript/dashboard/store/modules/conversations/getters.js b/frontend/app/javascript/dashboard/store/modules/conversations/getters.js index 745c3acc..1ee8508c 100644 --- a/frontend/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/frontend/app/javascript/dashboard/store/modules/conversations/getters.js @@ -1,5 +1,10 @@ import { MESSAGE_TYPE } from 'shared/constants/messages'; -import { applyPageFilters, applyRoleFilter, sortComparator } from './helpers'; +import { + applyPageFilters, + applyRoleFilter, + isConversationUnassigned, + sortComparator, +} from './helpers'; import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator'; import { matchesFilters } from './helpers/filterHelpers'; import { @@ -99,7 +104,7 @@ const getters = { }, getUnAssignedChats: _state => activeFilters => { return _state.allConversations.filter(conversation => { - const isUnAssigned = !conversation.meta.assignee; + const isUnAssigned = isConversationUnassigned(conversation); const shouldFilter = applyPageFilters(conversation, activeFilters); return isUnAssigned && shouldFilter; }); diff --git a/frontend/app/javascript/dashboard/store/modules/conversations/helpers.js b/frontend/app/javascript/dashboard/store/modules/conversations/helpers.js index 31432d7d..2affa4b4 100644 --- a/frontend/app/javascript/dashboard/store/modules/conversations/helpers.js +++ b/frontend/app/javascript/dashboard/store/modules/conversations/helpers.js @@ -11,6 +11,11 @@ export const findPendingMessageIndex = (chat, message) => { ); }; +export const isConversationUnassigned = conversation => { + const { assignee, assignee_type: assigneeType } = conversation.meta || {}; + return !assignee || assigneeType === 'AgentBot'; +}; + export const filterByStatus = (chatStatus, filterStatus) => filterStatus === 'all' ? true : chatStatus === filterStatus; @@ -95,7 +100,7 @@ export const applyRoleFilter = ( } const conversationAssignee = conversation.meta.assignee; - const isUnassigned = !conversationAssignee; + const isUnassigned = isConversationUnassigned(conversation); const isAssignedToUser = conversationAssignee?.id === currentUserId; // Check unassigned management permission diff --git a/frontend/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/frontend/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index a7ee3833..50ce0312 100644 --- a/frontend/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/frontend/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -159,6 +159,13 @@ describe('#getters', () => { meta: { team: { id: 5 } }, labels: ['sales'], }, + { + id: 33, + inbox_id: 4, + status: 1, + meta: { assignee: { id: 9 }, assignee_type: 'AgentBot' }, + labels: [], + }, ]; expect( @@ -180,6 +187,13 @@ describe('#getters', () => { meta: { team: { id: 5 } }, labels: ['sales'], }, + { + id: 33, + inbox_id: 4, + status: 1, + meta: { assignee: { id: 9 }, assignee_type: 'AgentBot' }, + labels: [], + }, ]); }); }); diff --git a/frontend/app/javascript/dashboard/store/modules/specs/conversations/helpers.spec.js b/frontend/app/javascript/dashboard/store/modules/specs/conversations/helpers.spec.js index b0a3cdab..e1f793f7 100644 --- a/frontend/app/javascript/dashboard/store/modules/specs/conversations/helpers.spec.js +++ b/frontend/app/javascript/dashboard/store/modules/specs/conversations/helpers.spec.js @@ -1,5 +1,6 @@ import { findPendingMessageIndex, + isConversationUnassigned, applyPageFilters, filterByInbox, filterByTeam, @@ -56,6 +57,24 @@ describe('#findPendingMessageIndex', () => { }); }); +describe('#isConversationUnassigned', () => { + it('treats an agent-bot conversation as unassigned for agent queues', () => { + expect( + isConversationUnassigned({ + meta: { assignee: { id: 7 }, assignee_type: 'AgentBot' }, + }) + ).toBe(true); + }); + + it('does not treat a human-assigned conversation as unassigned', () => { + expect( + isConversationUnassigned({ + meta: { assignee: { id: 7 }, assignee_type: 'User' }, + }) + ).toBe(false); + }); +}); + describe('#applyPageFilters', () => { describe('#filter-team', () => { it('returns true if conversation has team and team filter is active', () => {