fix(conversations): 修复状态切换与历史会话路由
- 处理历史主键与 display_id 冲突,确保消息和状态操作命中同一会话 - 允许已软删除收件箱中的历史会话切换状态 - 解决会话后自动打开列表中的相邻会话
This commit is contained in:
@@ -799,6 +799,27 @@ func (s *ConversationCrudTestSuite) TestToggleStatus_Success() {
|
||||
assert.Equal(s.T(), "resolved", resp.Payload.CurrentStatus)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestToggleStatus_SoftDeletedInboxStillUpdatesConversation() {
|
||||
s.Require().NoError(s.db.Delete(s.testInbox).Error)
|
||||
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"status": "resolved",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Require().Equal(http.StatusOK, w.Code, w.Body.String())
|
||||
var resp struct {
|
||||
Payload struct {
|
||||
CurrentStatus string `json:"current_status"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal("resolved", resp.Payload.CurrentStatus)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestToggleStatus_NoStatusTogglesLikeChatwoot() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", nil)
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -49,14 +50,29 @@ func (r *ConversationRepo) FindByAccountAndID(ctx context.Context, accountID, id
|
||||
}
|
||||
|
||||
// FindByAccountAndDisplayIDOrID retrieves a conversation using Chatwoot's
|
||||
// account-scoped display_id route semantics, falling back to primary key for
|
||||
// legacy GoChat data and tests that predate display_id.
|
||||
// account-scoped display_id route semantics, while preserving routes emitted
|
||||
// for legacy rows that still have a NULL/zero display_id. Legacy primary keys
|
||||
// must win before display_id lookup because the two identifier spaces can
|
||||
// collide after newer conversations receive account-scoped display IDs.
|
||||
func (r *ConversationRepo) FindByAccountAndDisplayIDOrID(ctx context.Context, accountID, routeID uint) (*model.Conversation, error) {
|
||||
var conversation model.Conversation
|
||||
err := r.db.WithContext(ctx).Where("account_id = ? AND display_id = ?", accountID, routeID).First(&conversation).Error
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND id = ? AND (display_id IS NULL OR display_id = 0)", accountID, routeID).
|
||||
First(&conversation).Error
|
||||
if err == nil {
|
||||
return &conversation, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.db.WithContext(ctx).Where("account_id = ? AND display_id = ?", accountID, routeID).First(&conversation).Error
|
||||
if err == nil {
|
||||
return &conversation, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.db.WithContext(ctx).Where("account_id = ? AND id = ?", accountID, routeID).First(&conversation).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -70,6 +70,26 @@ func TestCov2_ConversationRepo_FindByAccountAndDisplayIDOrID(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConversationRepo_FindByAccountAndDisplayIDOrID_PrefersLegacyNullDisplayIDPrimaryKey(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewConversationRepo(db)
|
||||
ctx := context.Background()
|
||||
account, inbox, contact := createConvAccount(t, db)
|
||||
|
||||
legacy := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
require.NoError(t, db.Create(legacy).Error)
|
||||
require.Nil(t, legacy.DisplayID)
|
||||
|
||||
collidingDisplayID := legacy.ID
|
||||
modern := &model.Conversation{DisplayID: &collidingDisplayID, AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
require.NoError(t, db.Create(modern).Error)
|
||||
|
||||
found, err := repo.FindByAccountAndDisplayIDOrID(ctx, account.ID, legacy.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, legacy.ID, found.ID)
|
||||
assert.Nil(t, found.DisplayID)
|
||||
}
|
||||
|
||||
func TestCov2_ConversationRepo_FindByContact(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewConversationRepo(db)
|
||||
|
||||
@@ -644,7 +644,7 @@ func (s *ConversationService) ToggleStatus(ctx context.Context, accountID, id ui
|
||||
|
||||
func (s *ConversationService) persistShangwutongConversationStatus(ctx context.Context, conversation *model.Conversation, previousStatus string) error {
|
||||
var inbox model.Inbox
|
||||
if err := s.repo.DB().WithContext(ctx).Select("id", "channel_type").First(&inbox, conversation.InboxID).Error; err != nil {
|
||||
if err := s.repo.DB().WithContext(ctx).Unscoped().Select("id", "channel_type").First(&inbox, conversation.InboxID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
metadata, _ := ctx.Value(shangwutongRequestMetadataKey{}).(shangwutongRequestMetadata)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -19,9 +20,20 @@ import {
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ConversationResolveAttributesModal from 'dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue';
|
||||
import {
|
||||
conversationListPageURL,
|
||||
} from 'dashboard/helper/URLHelper';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
isOnParticipatingView,
|
||||
isOnUnattendedView,
|
||||
isOnFoldersView,
|
||||
} from 'dashboard/store/modules/conversations/helpers/actionHelpers';
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { checkMissingAttributes } = useConversationRequiredAttributes();
|
||||
|
||||
@@ -35,6 +47,30 @@ const openDropdown = () => toggleDropdown(true);
|
||||
|
||||
const currentChat = computed(() => getters.getSelectedChat.value);
|
||||
|
||||
const redirectToConversationList = () => {
|
||||
const {
|
||||
params: { accountId, inbox_id: inboxId, label, teamId, id: customViewId },
|
||||
name,
|
||||
} = route;
|
||||
let conversationType = '';
|
||||
if (isOnMentionsView({ route: { name } })) conversationType = 'mention';
|
||||
else if (isOnParticipatingView({ route: { name } })) {
|
||||
conversationType = 'participating';
|
||||
} else if (isOnUnattendedView({ route: { name } })) {
|
||||
conversationType = 'unattended';
|
||||
}
|
||||
router.push(
|
||||
conversationListPageURL({
|
||||
accountId,
|
||||
conversationType,
|
||||
customViewId: isOnFoldersView({ route: { name } }) ? customViewId : 0,
|
||||
inboxId,
|
||||
label,
|
||||
teamId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const isOpen = computed(
|
||||
() => currentChat.value.status === wootConstants.STATUS_TYPE.OPEN
|
||||
);
|
||||
@@ -84,9 +120,13 @@ const openSnoozeModal = () => {
|
||||
const toggleStatus = (status, snoozedUntil, customAttributes = null) => {
|
||||
closeDropdown();
|
||||
isLoading.value = true;
|
||||
const conversationId = currentChat.value.id;
|
||||
const { all, activeIndex, lastIndex } = getConversationParams();
|
||||
const adjacentConversation =
|
||||
activeIndex < lastIndex ? all[activeIndex + 1] : all[activeIndex - 1];
|
||||
|
||||
const payload = {
|
||||
conversationId: currentChat.value.id,
|
||||
conversationId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
};
|
||||
@@ -95,10 +135,21 @@ const toggleStatus = (status, snoozedUntil, customAttributes = null) => {
|
||||
payload.customAttributes = customAttributes;
|
||||
}
|
||||
|
||||
store.dispatch('toggleStatus', payload).then(() => {
|
||||
useAlert(t('CONVERSATION.CHANGE_STATUS'));
|
||||
isLoading.value = false;
|
||||
});
|
||||
store
|
||||
.dispatch('toggleStatus', payload)
|
||||
.then(() => {
|
||||
useAlert(t('CONVERSATION.CHANGE_STATUS'));
|
||||
if (status !== wootConstants.STATUS_TYPE.OPEN) {
|
||||
if (adjacentConversation) {
|
||||
adjacentConversation.click();
|
||||
} else {
|
||||
redirectToConversationList();
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleResolveWithAttributes = ({ attributes, context }) => {
|
||||
|
||||
Reference in New Issue
Block a user