Compare commits

..
20 changed files with 183 additions and 221 deletions
@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from 'vitest';
import { usePortal } from '../usePortal';
import { useAccount } from 'dashboard/composables/useAccount';
import { useRoute } from 'dashboard/composables/route';
import { frontendURL } from 'dashboard/helper/URLHelper';
import allLocales from 'shared/constants/locales.js';
vi.mock('dashboard/composables/useAccount');
vi.mock('dashboard/composables/route');
vi.mock('dashboard/helper/URLHelper');
describe('usePortal', () => {
beforeEach(() => {
vi.mocked(useAccount).mockReturnValue({ accountId: { value: 123 } });
});
it('returns the correct properties', () => {
vi.mocked(useRoute).mockReturnValue({
params: { portalSlug: 'test-portal', locale: 'en' },
});
const portal = usePortal();
expect(portal).toHaveProperty('accountId');
expect(portal).toHaveProperty('portalSlug');
expect(portal).toHaveProperty('locale');
expect(portal).toHaveProperty('articleUrl');
expect(portal).toHaveProperty('localeName');
});
it('computes portalSlug and locale correctly', () => {
vi.mocked(useRoute).mockReturnValue({
params: { portalSlug: 'test-portal', locale: 'fr' },
});
const { portalSlug, locale } = usePortal();
expect(portalSlug.value).toBe('test-portal');
expect(locale.value).toBe('fr');
});
it('generates correct article URL', () => {
vi.mocked(useAccount).mockReturnValue({ accountId: { value: 456 } });
vi.mocked(useRoute).mockReturnValue({
params: { portalSlug: 'help-center', locale: 'es' },
});
vi.mocked(frontendURL).mockReturnValue('https://example.com/article');
const { articleUrl } = usePortal();
const url = articleUrl(789);
expect(frontendURL).toHaveBeenCalledWith(
'accounts/456/portals/help-center/es/articles/789'
);
expect(url).toBe('https://example.com/article');
});
it('returns correct locale name', () => {
vi.mocked(useRoute).mockReturnValue({
params: { portalSlug: 'test-portal', locale: 'ja' },
});
const { localeName } = usePortal();
const name = localeName('ja');
expect(name).toBe(allLocales.ja);
});
});
@@ -0,0 +1,53 @@
import { useAccount } from 'dashboard/composables/useAccount';
import { frontendURL } from 'dashboard/helper/URLHelper';
import allLocales from 'shared/constants/locales.js';
import { useRoute } from 'dashboard/composables/route';
import { computed } from 'vue';
/**
* @typedef {Object} PortalComposable
* @property {import('vue').ComputedRef<number>} accountId - The current account ID.
* @property {import('vue').ComputedRef<string>} portalSlug - The slug of the current portal.
* @property {import('vue').ComputedRef<string>} locale - The current locale code.
* @property {function(number): string} articleUrl - A function to generate the URL for an article.
* @property {function(string): string} localeName - A function to get the localized name of a locale.
*/
/**
* A composable for managing portal-related data and utilities.
* @returns {PortalComposable} An object containing portal-related properties and functions.
*/
export const usePortal = () => {
const { accountId } = useAccount();
const route = useRoute();
const portalSlug = computed(() => route.params.portalSlug);
const locale = computed(() => route.params.locale);
/**
* Generates the URL for an article.
* @param {number} id - The ID of the article.
* @returns {string} The full URL for the article.
*/
const articleUrl = id => {
return frontendURL(
`accounts/${accountId.value}/portals/${portalSlug.value}/${locale.value}/articles/${id}`
);
};
/**
* Gets the localized name of a locale.
* @param {string} code - The locale code.
* @returns {string} The localized name of the locale.
*/
const localeName = code => {
return allLocales[code];
};
return {
accountId,
portalSlug,
locale,
articleUrl,
localeName,
};
};
@@ -1,14 +1,14 @@
<script>
import { dynamicTime } from 'shared/helpers/timeHelper';
import portalMixin from '../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
import { frontendURL } from 'dashboard/helper/URLHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { useAccount } from 'dashboard/composables/useAccount';
export default {
components: {
Thumbnail,
},
mixins: [portalMixin],
props: {
showDragIcon: {
type: Boolean,
@@ -44,7 +44,11 @@ export default {
default: 0,
},
},
setup() {
const { accountId } = useAccount();
const { articleUrl, portalSlug, locale, localeName } = usePortal();
return { accountId, portalSlug, locale, localeName, articleUrl };
},
computed: {
lastUpdatedAt() {
return dynamicTime(this.updatedAt);
@@ -7,7 +7,7 @@ import SearchResults from './SearchResults.vue';
import ArticleView from './ArticleView.vue';
import ArticlesAPI from 'dashboard/api/helpCenter/articles';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import portalMixin from '../../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
export default {
name: 'ArticleSearchPopover',
@@ -16,13 +16,16 @@ export default {
SearchResults,
ArticleView,
},
mixins: [portalMixin],
props: {
selectedPortalSlug: {
type: String,
required: true,
},
},
setup() {
const { localeName } = usePortal();
return { localeName };
},
data() {
return {
searchQuery: '',
@@ -10,7 +10,8 @@ import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShor
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import portalMixin from '../mixins/portalMixin';
import { useAccount } from 'dashboard/composables/useAccount';
import { usePortal } from 'dashboard/composables/usePortal';
import AddCategory from '../pages/categories/AddCategory.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const CommandBar = () =>
@@ -28,13 +29,16 @@ export default {
UpgradePage,
WootKeyShortcutModal,
},
mixins: [portalMixin],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { accountId } = useAccount();
const { locale, localeName } = usePortal();
return {
uiSettings,
updateUISettings,
accountId,
locale,
localeName,
};
},
data() {
@@ -51,7 +55,6 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
portals: 'portals/allPortals',
categories: 'categories/allCategories',
meta: 'portals/getMeta',
@@ -1,7 +1,6 @@
<script>
import portalMixin from '../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
export default {
mixins: [portalMixin],
props: {
locales: {
type: Array,
@@ -12,7 +11,10 @@ export default {
default: '',
},
},
setup() {
const { localeName } = usePortal();
return { localeName };
},
methods: {
changeDefaultLocale(localeCode) {
this.$emit('changeDefaultLocale', { localeCode });
@@ -1,12 +1,11 @@
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import portalMixin from '../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
export default {
components: {
Thumbnail,
},
mixins: [portalMixin],
props: {
portal: {
type: Object,
@@ -25,6 +24,10 @@ export default {
default: '',
},
},
setup() {
const { localeName } = usePortal();
return { localeName };
},
data() {
return {
selectedLocale: null,
@@ -1,24 +0,0 @@
import { mapGetters } from 'vuex';
import { frontendURL } from 'dashboard/helper/URLHelper';
import allLocales from 'shared/constants/locales.js';
export default {
computed: {
...mapGetters({ accountId: 'getCurrentAccountId' }),
portalSlug() {
return this.$route.params.portalSlug;
},
locale() {
return this.$route.params.locale;
},
},
methods: {
articleUrl(id) {
return frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/${id}`
);
},
localeName(code) {
return allLocales[code];
},
},
};
@@ -1,76 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import portalMixin from '../portalMixin';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueRouter);
import ListAllArticles from '../../pages/portals/ListAllPortals.vue';
const router = new VueRouter({
routes: [
{
path: ':portalSlug/:locale/articles',
name: 'list_all_locale_articles',
component: ListAllArticles,
},
],
});
describe('portalMixin', () => {
let getters;
let store;
let wrapper;
beforeEach(() => {
getters = {
getCurrentAccountId: () => 1,
};
const Component = {
render() {},
title: 'TestComponent',
mixins: [portalMixin],
router,
};
store = new Vuex.Store({ getters });
wrapper = shallowMount(Component, { store, localVue });
});
it('return account id', () => {
expect(wrapper.vm.accountId).toBe(1);
});
it('returns article url', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'en' },
});
expect(wrapper.vm.articleUrl(1)).toBe(
'/app/accounts/1/portals/fur-rent/en/articles/1'
);
});
it('returns portal locale', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'es' },
});
expect(wrapper.vm.portalSlug).toBe('fur-rent');
});
it('returns portal slug', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'campaign', locale: 'es' },
});
expect(wrapper.vm.portalSlug).toBe('campaign');
});
it('returns locale name', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'es' },
});
expect(wrapper.vm.localeName('es')).toBe('Spanish');
});
});
@@ -5,7 +5,7 @@ import EditArticleHeader from '../../components/Header/EditArticleHeader.vue';
import ArticleEditor from '../../components/ArticleEditor.vue';
import ArticleSettings from './ArticleSettings.vue';
import Spinner from 'shared/components/Spinner.vue';
import portalMixin from '../../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
import wootConstants from 'dashboard/constants/globals';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
@@ -18,7 +18,10 @@ export default {
Spinner,
ArticleSettings,
},
mixins: [portalMixin],
setup() {
const { locale } = usePortal();
return { locale };
},
data() {
return {
isUpdating: false,
@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader.vue';
import ArticleEditor from '../../components/ArticleEditor.vue';
import portalMixin from '../../mixins/portalMixin';
import { usePortal } from 'dashboard/composables/usePortal';
import ArticleSettings from './ArticleSettings.vue';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
@@ -12,7 +12,10 @@ export default {
ArticleEditor,
ArticleSettings,
},
mixins: [portalMixin],
setup() {
const { locale } = usePortal();
return { locale };
},
data() {
return {
articleTitle: '',
+3 -4
View File
@@ -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 useReplaceRoute from 'widget/composables/useReplaceRoute';
import routerMixin from './mixins/routerMixin';
import {
getExtraSpaceToScroll,
loadedEventConfig,
@@ -27,11 +27,10 @@ export default {
components: {
Spinner,
},
mixins: [availabilityMixin, configMixin],
mixins: [availabilityMixin, configMixin, routerMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
const replaceRoute = useReplaceRoute();
return { replaceRoute, prefersDarkMode };
return { prefersDarkMode };
},
data() {
return {
@@ -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 useReplaceRoute from 'widget/composables/useReplaceRoute';
import routerMixin from 'widget/mixins/routerMixin';
import { IFrameHelper } from '../helpers/utils';
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
@@ -16,10 +16,7 @@ export default {
CustomButton,
FooterReplyTo,
},
setup() {
const replaceRoute = useReplaceRoute();
return { replaceRoute };
},
mixins: [routerMixin],
data() {
return {
inReplyTo: null,
@@ -3,8 +3,6 @@ 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 useReplaceRoute from 'widget/composables/useReplaceRoute';
import darkMixin from 'widget/mixins/darkModeMixin.js';
import routerMixin from 'widget/mixins/routerMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
@@ -14,7 +12,7 @@ export default {
FluentIcon,
HeaderActions,
},
mixins: [nextAvailabilityTime, availabilityMixin],
mixins: [nextAvailabilityTime, availabilityMixin, routerMixin],
props: {
avatarUrl: {
type: String,
@@ -38,9 +36,8 @@ export default {
},
},
setup() {
const replaceRoute = useReplaceRoute();
const { getThemeClass } = useDarkMode();
return { getThemeClass, replaceRoute };
return { getThemeClass };
},
computed: {
isOnline() {
@@ -5,20 +5,17 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import { isEmptyObject } from 'widget/helpers/utils';
import { getRegexp } from 'shared/helpers/Validators';
import useReplaceRoute from 'widget/composables/useReplaceRoute';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import routerMixin from 'widget/mixins/routerMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import configMixin from 'widget/mixins/configMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
export default {
components: {
CustomButton,
Spinner,
},
mixins: [configMixin],
mixins: [routerMixin, configMixin],
props: {
options: {
type: Object,
@@ -27,9 +24,8 @@ export default {
},
setup() {
const { formatMessage } = useMessageFormatter();
const replaceRoute = useReplaceRoute();
const { getThemeClass } = useDarkMode();
return { formatMessage, getThemeClass, replaceRoute };
return { formatMessage, getThemeClass };
},
data() {
return {
@@ -1,51 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import { useReplaceRoute } from '../useReplaceRoute';
vi.mock('dashboard/composables/route', () => ({
useRouter: vi.fn(),
useRoute: vi.fn(),
}));
import { useRouter, useRoute } from 'dashboard/composables/route';
describe('useReplaceRoute', () => {
const mockReplace = vi.fn();
const mockRouter = { replace: mockReplace };
beforeEach(() => {
vi.clearAllMocks();
useRouter.mockReturnValue(mockRouter);
});
it('should replace route when current route is different', () => {
useRoute.mockReturnValue({ name: 'currentRoute' });
const replaceRoute = useReplaceRoute();
replaceRoute('newRoute', { id: 1 });
expect(mockReplace).toHaveBeenCalledWith({
name: 'newRoute',
params: { id: 1 },
});
});
it('should not replace route when current route is the same', () => {
useRoute.mockReturnValue({ name: 'sameRoute' });
const replaceRoute = useReplaceRoute();
const result = replaceRoute('sameRoute');
expect(mockReplace).not.toHaveBeenCalled();
expect(result).toBeUndefined();
});
it('should return the result of router.replace when replacing route', () => {
useRoute.mockReturnValue({ name: 'currentRoute' });
mockReplace.mockReturnValue('replacementResult');
const replaceRoute = useReplaceRoute();
const result = replaceRoute('newRoute');
expect(result).toBe('replacementResult');
});
});
@@ -1,21 +0,0 @@
import { useRouter, useRoute } from 'dashboard/composables/route';
/**
* Composable for replacing 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 {Function} A function that replaces the route when called.
*/
export const useReplaceRoute = () => {
const router = useRouter();
const route = useRoute();
return (name, params = {}) => {
if (route.name !== name) {
return router.replace({ name, params });
}
return undefined;
};
};
export default useReplaceRoute;
@@ -0,0 +1,10 @@
export default {
methods: {
async replaceRoute(name, params = {}) {
if (this.$route.name !== name) {
return this.$router.replace({ name, params });
}
return undefined;
},
},
};
+2 -5
View File
@@ -4,8 +4,6 @@ import ArticleHero from 'widget/components/ArticleHero.vue';
import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoader.vue';
import { mapGetters } from 'vuex';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import useReplaceRoute from 'widget/composables/useReplaceRoute';
import { useDarkMode } from 'widget/composables/useDarkMode';
import routerMixin from 'widget/mixins/routerMixin';
import configMixin from 'widget/mixins/configMixin';
@@ -17,11 +15,10 @@ export default {
TeamAvailability,
ArticleCardSkeletonLoader,
},
mixins: [configMixin],
mixins: [configMixin, routerMixin],
setup() {
const replaceRoute = useReplaceRoute();
const { prefersDarkMode } = useDarkMode();
return { prefersDarkMode, replaceRoute };
return { prefersDarkMode };
},
computed: {
...mapGetters({
+2 -6
View File
@@ -1,7 +1,7 @@
<script>
import PreChatForm from '../components/PreChat/Form.vue';
import configMixin from '../mixins/configMixin';
import useReplaceRoute from 'widget/composables/useReplaceRoute';
import routerMixin from '../mixins/routerMixin';
import { isEmptyObject } from 'widget/helpers/utils';
import { ON_CONVERSATION_CREATED } from '../constants/widgetBusEvents';
@@ -9,11 +9,7 @@ export default {
components: {
PreChatForm,
},
mixins: [configMixin],
setup() {
const replaceRoute = useReplaceRoute();
return { replaceRoute };
},
mixins: [configMixin, routerMixin],
mounted() {
this.$emitter.on(ON_CONVERSATION_CREATED, () => {
// Redirect to messages page after conversation is created