mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 12:37:56 +00:00
fix: Prevent duplicate chat creation in the web widget during latency (#10745)
This commit is contained in:
@@ -24,7 +24,8 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
buttonClassName() {
|
buttonClassName() {
|
||||||
let className = 'text-white py-3 px-4 rounded shadow-sm leading-4';
|
let className =
|
||||||
|
'text-white py-3 px-4 rounded shadow-sm leading-4 cursor-pointer disabled:opacity-50';
|
||||||
if (this.type === 'clear') {
|
if (this.type === 'clear') {
|
||||||
className = 'flex mx-auto mt-4 text-xs leading-3 w-auto text-black-600';
|
className = 'flex mx-auto mt-4 text-xs leading-3 w-auto text-black-600';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,13 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
widgetColor: 'appConfig/getWidgetColor',
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
isCreating: 'conversation/getIsCreating',
|
isCreating: 'conversation/getIsCreating',
|
||||||
|
isConversationRouting: 'appConfig/getIsUpdatingRoute',
|
||||||
activeCampaign: 'campaign/getActiveCampaign',
|
activeCampaign: 'campaign/getActiveCampaign',
|
||||||
currentUser: 'contacts/getCurrentUser',
|
currentUser: 'contacts/getCurrentUser',
|
||||||
}),
|
}),
|
||||||
|
isCreatingConversation() {
|
||||||
|
return this.isCreating || this.isConversationRouting;
|
||||||
|
},
|
||||||
textColor() {
|
textColor() {
|
||||||
return getContrastingTextColor(this.widgetColor);
|
return getContrastingTextColor(this.widgetColor);
|
||||||
},
|
},
|
||||||
@@ -337,9 +341,9 @@ export default {
|
|||||||
block
|
block
|
||||||
:bg-color="widgetColor"
|
:bg-color="widgetColor"
|
||||||
:text-color="textColor"
|
:text-color="textColor"
|
||||||
:disabled="isCreating"
|
:disabled="isCreatingConversation"
|
||||||
>
|
>
|
||||||
<Spinner v-if="isCreating" class="p-0" />
|
<Spinner v-if="isCreatingConversation" class="p-0" />
|
||||||
{{ $t('START_CONVERSATION') }}
|
{{ $t('START_CONVERSATION') }}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import ViewWithHeader from './components/layouts/ViewWithHeader.vue';
|
import ViewWithHeader from './components/layouts/ViewWithHeader.vue';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
export default createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
@@ -42,3 +43,35 @@ export default createRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation Guards to Handle Route Transitions
|
||||||
|
*
|
||||||
|
* Purpose:
|
||||||
|
* Prevents duplicate form submissions and API calls during route transitions,
|
||||||
|
* especially important in high-latency scenarios.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. beforeEach: Sets isUpdatingRoute to true at start of navigation
|
||||||
|
* 2. Component buttons/actions check this flag to prevent duplicate actions
|
||||||
|
* 3. afterEach: Resets the flag once navigation is complete
|
||||||
|
*
|
||||||
|
* Implementation note:
|
||||||
|
* Handling it globally, so that we can use it across all components
|
||||||
|
* to ensure consistent UI behavior during all route transitions.
|
||||||
|
*
|
||||||
|
* @see https://github.com/chatwoot/chatwoot/issues/10736
|
||||||
|
*/
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
// Prevent any user interactions during route transition
|
||||||
|
await store.dispatch('appConfig/setRouteTransitionState', true);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
// Re-enable user interactions after navigation is complete
|
||||||
|
store.dispatch('appConfig/setRouteTransitionState', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
SET_WIDGET_APP_CONFIG,
|
SET_WIDGET_APP_CONFIG,
|
||||||
SET_WIDGET_COLOR,
|
SET_WIDGET_COLOR,
|
||||||
TOGGLE_WIDGET_OPEN,
|
TOGGLE_WIDGET_OPEN,
|
||||||
|
SET_ROUTE_UPDATE_STATE,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -19,6 +20,7 @@ const state = {
|
|||||||
widgetColor: '',
|
widgetColor: '',
|
||||||
widgetStyle: 'standard',
|
widgetStyle: 'standard',
|
||||||
darkMode: 'light',
|
darkMode: 'light',
|
||||||
|
isUpdatingRoute: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -31,6 +33,7 @@ export const getters = {
|
|||||||
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
||||||
darkMode: $state => $state.darkMode,
|
darkMode: $state => $state.darkMode,
|
||||||
getShowUnreadMessagesDialog: $state => $state.showUnreadMessagesDialog,
|
getShowUnreadMessagesDialog: $state => $state.showUnreadMessagesDialog,
|
||||||
|
getIsUpdatingRoute: _state => _state.isUpdatingRoute,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@@ -69,6 +72,13 @@ export const actions = {
|
|||||||
setBubbleVisibility({ commit }, hideMessageBubble) {
|
setBubbleVisibility({ commit }, hideMessageBubble) {
|
||||||
commit(SET_BUBBLE_VISIBILITY, hideMessageBubble);
|
commit(SET_BUBBLE_VISIBILITY, hideMessageBubble);
|
||||||
},
|
},
|
||||||
|
setRouteTransitionState: async ({ commit }, status) => {
|
||||||
|
// Handles the routing state during navigation to different screen
|
||||||
|
// Called before the navigation starts and after navigation completes
|
||||||
|
// Handling this state in app/javascript/widget/router.js
|
||||||
|
// See issue: https://github.com/chatwoot/chatwoot/issues/10736
|
||||||
|
commit(SET_ROUTE_UPDATE_STATE, status);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
@@ -96,6 +106,9 @@ export const mutations = {
|
|||||||
[SET_COLOR_SCHEME]($state, darkMode) {
|
[SET_COLOR_SCHEME]($state, darkMode) {
|
||||||
$state.darkMode = darkMode;
|
$state.darkMode = darkMode;
|
||||||
},
|
},
|
||||||
|
[SET_ROUTE_UPDATE_STATE]($state, status) {
|
||||||
|
$state.isUpdatingRoute = status;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -31,4 +31,11 @@ describe('#actions', () => {
|
|||||||
expect(commit.mock.calls).toEqual([['SET_COLOR_SCHEME', 'dark']]);
|
expect(commit.mock.calls).toEqual([['SET_COLOR_SCHEME', 'dark']]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setRouteTransitionState', () => {
|
||||||
|
it('creates actions properly', () => {
|
||||||
|
actions.setRouteTransitionState({ commit }, false);
|
||||||
|
expect(commit.mock.calls).toEqual([['SET_ROUTE_UPDATE_STATE', false]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,4 +19,10 @@ describe('#getters', () => {
|
|||||||
expect(getters.getShowUnreadMessagesDialog(state)).toEqual(true);
|
expect(getters.getShowUnreadMessagesDialog(state)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('#getIsUpdatingRoute', () => {
|
||||||
|
it('returns correct value', () => {
|
||||||
|
const state = { isUpdatingRoute: true };
|
||||||
|
expect(getters.getIsUpdatingRoute(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,4 +32,12 @@ describe('#mutations', () => {
|
|||||||
expect(state.darkMode).toEqual('dark');
|
expect(state.darkMode).toEqual('dark');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#SET_ROUTE_UPDATE_STATE', () => {
|
||||||
|
it('sets dark mode properly', () => {
|
||||||
|
const state = { isUpdatingRoute: false };
|
||||||
|
mutations.SET_ROUTE_UPDATE_STATE(state, true);
|
||||||
|
expect(state.isUpdatingRoute).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('#actions', () => {
|
|||||||
messages: [{ id: 1, content: 'This is a test message' }],
|
messages: [{ id: 1, content: 'This is a test message' }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let windowSpy = vi.spyOn(window, 'window', 'get');
|
let windowSpy = vi.spyOn(window, 'window', 'get');
|
||||||
windowSpy.mockImplementation(() => ({
|
windowSpy.mockImplementation(() => ({
|
||||||
WOOT_WIDGET: {
|
WOOT_WIDGET: {
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
|
|||||||
export const TOGGLE_WIDGET_OPEN = 'TOGGLE_WIDGET_OPEN';
|
export const TOGGLE_WIDGET_OPEN = 'TOGGLE_WIDGET_OPEN';
|
||||||
export const SET_REFERRER_HOST = 'SET_REFERRER_HOST';
|
export const SET_REFERRER_HOST = 'SET_REFERRER_HOST';
|
||||||
export const SET_BUBBLE_VISIBILITY = 'SET_BUBBLE_VISIBILITY';
|
export const SET_BUBBLE_VISIBILITY = 'SET_BUBBLE_VISIBILITY';
|
||||||
|
export const SET_ROUTE_UPDATE_STATE = 'SET_ROUTE_UPDATE_STATE';
|
||||||
|
|||||||
@@ -12,12 +12,20 @@ export default {
|
|||||||
},
|
},
|
||||||
mixins: [configMixin, routerMixin],
|
mixins: [configMixin, routerMixin],
|
||||||
mounted() {
|
mounted() {
|
||||||
emitter.on(ON_CONVERSATION_CREATED, () => {
|
// Register event listener for conversation creation
|
||||||
// Redirect to messages page after conversation is created
|
emitter.on(ON_CONVERSATION_CREATED, this.handleConversationCreated);
|
||||||
this.replaceRoute('messages');
|
},
|
||||||
});
|
beforeUnmount() {
|
||||||
|
emitter.off(ON_CONVERSATION_CREATED, this.handleConversationCreated);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleConversationCreated() {
|
||||||
|
// Redirect to messages page after conversation is created
|
||||||
|
this.replaceRoute('messages');
|
||||||
|
// Only after successful navigation, reset the isUpdatingRoute UIflag in app/javascript/widget/router.js
|
||||||
|
// See issue: https://github.com/chatwoot/chatwoot/issues/10736
|
||||||
|
},
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
fullName,
|
fullName,
|
||||||
emailAddress,
|
emailAddress,
|
||||||
|
|||||||
Reference in New Issue
Block a user