diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 6645465c5..892046f37 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -8,7 +8,7 @@ import availabilityMixin from 'widget/mixins/availability'; import { getLocale } from './helpers/urlParamsHelper'; import { isEmptyObject } from 'widget/helpers/utils'; import Spinner from 'shared/components/Spinner.vue'; -import routerMixin from './mixins/routerMixin'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import { getExtraSpaceToScroll, loadedEventConfig, @@ -27,7 +27,11 @@ export default { components: { Spinner, }, - mixins: [availabilityMixin, configMixin, routerMixin, darkModeMixin], + mixins: [availabilityMixin, configMixin, darkModeMixin], + setup() { + const { replaceRoute } = useRouterHelper(); + return { replaceRoute }; + }, data() { return { isMobile: false, diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index ab6b396e8..20401dbf8 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue'; import ChatInputWrap from 'widget/components/ChatInputWrap.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import { sendEmailTranscript } from 'widget/api/conversation'; -import routerMixin from 'widget/mixins/routerMixin'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import { IFrameHelper } from '../helpers/utils'; import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents'; @@ -16,7 +16,10 @@ export default { CustomButton, FooterReplyTo, }, - mixins: [routerMixin], + setup() { + const { replaceRoute } = useRouterHelper(); + return { replaceRoute }; + }, data() { return { inReplyTo: null, diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 5083f6ef5..1bff57648 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -3,7 +3,7 @@ import availabilityMixin from 'widget/mixins/availability'; import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import HeaderActions from './HeaderActions.vue'; -import routerMixin from 'widget/mixins/routerMixin'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import darkMixin from 'widget/mixins/darkModeMixin.js'; export default { @@ -12,7 +12,7 @@ export default { FluentIcon, HeaderActions, }, - mixins: [nextAvailabilityTime, availabilityMixin, routerMixin, darkMixin], + mixins: [nextAvailabilityTime, availabilityMixin, darkMixin], props: { avatarUrl: { type: String, @@ -35,6 +35,10 @@ export default { default: () => {}, }, }, + setup() { + const { replaceRoute } = useRouterHelper(); + return { replaceRoute }; + }, computed: { isOnline() { const { workingHoursEnabled } = this.channelConfig; diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 596ba16d9..3a0e5e6f4 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -6,7 +6,7 @@ import { getContrastingTextColor } from '@chatwoot/utils'; import { isEmptyObject } from 'widget/helpers/utils'; import { getRegexp } from 'shared/helpers/Validators'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; -import routerMixin from 'widget/mixins/routerMixin'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import darkModeMixin from 'widget/mixins/darkModeMixin'; import configMixin from 'widget/mixins/configMixin'; @@ -15,13 +15,17 @@ export default { CustomButton, Spinner, }, - mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin], + mixins: [darkModeMixin, messageFormatterMixin, configMixin], props: { options: { type: Object, default: () => {}, }, }, + setup() { + const { replaceRoute } = useRouterHelper(); + return { replaceRoute }; + }, data() { return { locale: this.$root.$i18n.locale, diff --git a/app/javascript/widget/composables/useRouterHelper.js b/app/javascript/widget/composables/useRouterHelper.js new file mode 100644 index 000000000..eaa2d0220 --- /dev/null +++ b/app/javascript/widget/composables/useRouterHelper.js @@ -0,0 +1,26 @@ +import { useRouter, useRoute } from 'dashboard/composables/route'; + +/** + * Composable for handling router-related operations. + * @returns {Object} An object containing methods for router manipulation. + */ +export function useRouterHelper() { + const router = useRouter(); + const route = useRoute(); + /** + * Replaces the current route with a new one if it's different. + * @param {string} name - The name of the route to replace with. + * @param {Object} params - The params to pass to the new route. + * @returns {Promise|undefined} A promise that resolves when the navigation is complete, or undefined if no navigation occurs. + */ + const replaceRoute = async (name, params = {}) => { + if (route.name !== name) { + return router.replace({ name, params }); + } + return undefined; + }; + + return { + replaceRoute, + }; +} diff --git a/app/javascript/widget/mixins/routerMixin.js b/app/javascript/widget/mixins/routerMixin.js deleted file mode 100644 index b3b37f6fd..000000000 --- a/app/javascript/widget/mixins/routerMixin.js +++ /dev/null @@ -1,10 +0,0 @@ -export default { - methods: { - async replaceRoute(name, params = {}) { - if (this.$route.name !== name) { - return this.$router.replace({ name, params }); - } - return undefined; - }, - }, -}; diff --git a/app/javascript/widget/mixins/specs/useRouterHelper.spec.js b/app/javascript/widget/mixins/specs/useRouterHelper.spec.js new file mode 100644 index 000000000..6a2247f72 --- /dev/null +++ b/app/javascript/widget/mixins/specs/useRouterHelper.spec.js @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useRouterHelper } from '../../composables/useRouterHelper'; + +const mockRouter = { + currentRoute: { name: 'initialRoute' }, + replace: vi.fn(), +}; + +vi.mock('../../composables/useRouterHelper', () => ({ + useRouterHelper: () => ({ + replaceRoute: async (name, params = {}) => { + if (mockRouter.currentRoute.name !== name) { + return mockRouter.replace({ name, params }); + } + return undefined; + }, + }), +})); + +describe('useRouterHelper', () => { + let routerHelper; + + beforeEach(() => { + vi.resetAllMocks(); + mockRouter.currentRoute = { name: 'initialRoute' }; + routerHelper = useRouterHelper(); + }); + + describe('replaceRoute', () => { + it('should replace route when current route is different', async () => { + mockRouter.replace.mockResolvedValue(undefined); + + await routerHelper.replaceRoute('newRoute', { id: 1 }); + + expect(mockRouter.replace).toHaveBeenCalledWith({ + name: 'newRoute', + params: { id: 1 }, + }); + }); + + it('should not replace route when current route is the same', async () => { + mockRouter.currentRoute.name = 'sameRoute'; + + const result = await routerHelper.replaceRoute('sameRoute'); + + expect(mockRouter.replace).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('should handle router replace rejection', async () => { + const error = new Error('Navigation aborted'); + mockRouter.replace.mockRejectedValue(error); + + await expect(routerHelper.replaceRoute('newRoute')).rejects.toThrow( + 'Navigation aborted' + ); + }); + }); +}); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index 5695f9e65..60e4449cc 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -5,7 +5,7 @@ import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoad import { mapGetters } from 'vuex'; import darkModeMixin from 'widget/mixins/darkModeMixin'; -import routerMixin from 'widget/mixins/routerMixin'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import configMixin from 'widget/mixins/configMixin'; export default { @@ -15,7 +15,11 @@ export default { TeamAvailability, ArticleCardSkeletonLoader, }, - mixins: [configMixin, routerMixin, darkModeMixin], + mixins: [configMixin, darkModeMixin], + setup() { + const { replaceRoute } = useRouterHelper(); + return { replaceRoute }; + }, computed: { ...mapGetters({ availableAgents: 'agent/availableAgents', diff --git a/app/javascript/widget/views/PreChatForm.vue b/app/javascript/widget/views/PreChatForm.vue index cb122a2f3..43417e8c3 100644 --- a/app/javascript/widget/views/PreChatForm.vue +++ b/app/javascript/widget/views/PreChatForm.vue @@ -1,7 +1,7 @@