fix: keep pending widget conversations visible and searchable
Build and publish Docker images / Build and publish images (push) Successful in 2m25s

This commit is contained in:
2026-09-14 17:30:24 +08:00
parent ca2a3c2dee
commit 665e531dc3
8 changed files with 74 additions and 4 deletions
+1
View File
@@ -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)
@@ -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
}
@@ -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)
}
@@ -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];
}
@@ -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;
});
@@ -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
@@ -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: [],
},
]);
});
});
@@ -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', () => {