Merge branch 'develop' into feat/captain-inbox-selector

This commit is contained in:
Pranav
2024-12-09 15:20:21 -08:00
2969 changed files with 77734 additions and 55041 deletions
@@ -31,26 +31,29 @@ export default {
currentAccountId: 'getCurrentAccountId',
currentUserAutoOffline: 'getCurrentUserAutoOffline',
}),
statusList() {
return [
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.ONLINE'),
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.BUSY'),
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.OFFLINE'),
];
},
availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
key => key === this.currentUserAvailability
);
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[
availabilityIndex
];
return this.statusList[availabilityIndex];
},
currentUserAvailability() {
return this.getCurrentUserAvailability;
},
availabilityStatuses() {
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
(statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
})
);
return this.statusList.map((statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
}));
},
},
@@ -102,7 +105,7 @@ export default {
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class-names="status-change--dropdown-button"
class="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
<AvailabilityStatusBadge :status="status.value" />
@@ -129,7 +132,7 @@ export default {
<woot-switch
size="small"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
:model-value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</WootDropdownItem>
@@ -1,14 +1,17 @@
<script>
import { ref } from 'vue';
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useRoute, useRouter } from 'dashboard/composables/route';
import { useAccount } from 'dashboard/composables/useAccount';
import { useRoute, useRouter } from 'vue-router';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import { routesWithPermissions } from '../../routes';
import { hasPermissions } from '../../helper/permissionsHelper';
import {
getUserPermissions,
hasPermissions,
} from '../../helper/permissionsHelper';
export default {
components: {
@@ -20,15 +23,18 @@ export default {
type: Boolean,
default: true,
},
sidebarClassName: {
type: String,
default: '',
},
},
emits: [
'toggleAccountModal',
'showAddLabelPopup',
'openNotificationPanel',
'closeKeyShortcutModal',
'openKeyShortcutModal',
],
setup(props, { emit }) {
const sidebarRef = ref(null);
const route = useRoute();
const router = useRouter();
const { accountId } = useAccount();
const toggleKeyShortcutModal = () => {
emit('openKeyShortcutModal');
@@ -64,11 +70,11 @@ export default {
action: () => navigateToRoute('agent_list'),
},
};
useKeyboardEvents(keyboardEvents, sidebarRef);
useKeyboardEvents(keyboardEvents);
return {
toggleKeyShortcutModal,
sidebarRef,
accountId,
};
},
data() {
@@ -79,7 +85,6 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
@@ -99,6 +104,9 @@ export default {
return '';
},
customViews() {
if (!this.activeCustomView) {
return [];
}
return this.$store.getters['customViews/getCustomViewsByFilterType'](
this.activeCustomView
);
@@ -113,7 +121,10 @@ export default {
return getSidebarItems(this.accountId);
},
primaryMenuItems() {
const userPermissions = this.currentUser.permissions;
const userPermissions = getUserPermissions(
this.currentUser,
this.accountId
);
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => {
const isAvailableForTheUser = hasPermissions(
@@ -156,6 +167,17 @@ export default {
) || {};
return activePrimaryMenu;
},
hasSecondaryMenu() {
return (
this.activeSecondaryMenu.menuItems &&
this.activeSecondaryMenu.menuItems.length
);
},
hasSecondarySidebar() {
// if it is explicitly stated to show and it has secondary menu items to show
// showSecondarySidebar corresponds to the UI settings, indicating if the user has toggled it
return this.showSecondarySidebar && this.hasSecondaryMenu;
},
},
watch: {
@@ -195,7 +217,7 @@ export default {
</script>
<template>
<aside ref="sidebarRef" class="flex h-full">
<aside class="flex h-full">
<PrimarySidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
@@ -203,13 +225,12 @@ export default {
:account-id="accountId"
:menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key"
@toggleAccounts="toggleAccountModal"
@openKeyShortcutModal="toggleKeyShortcutModal"
@openNotificationPanel="openNotificationPanel"
@toggle-accounts="toggleAccountModal"
@open-key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<SecondarySidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
v-if="hasSecondarySidebar"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
@@ -218,8 +239,8 @@ export default {
:menu-config="activeSecondaryMenu"
:current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@addLabel="showAddLabelPopup"
@toggleAccounts="toggleAccountModal"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</aside>
</template>
@@ -2,23 +2,23 @@ import { frontendURL } from '../../../../helper/URLHelper';
const campaigns = accountId => ({
parentNav: 'campaigns',
routes: ['ongoing_campaigns', 'one_off'],
routes: ['campaigns_sms_index', 'campaigns_livechat_index'],
menuItems: [
{
icon: 'arrow-swap',
label: 'ONGOING',
label: 'LIVE_CHAT',
key: 'ongoingCampaigns',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
toStateName: 'ongoing_campaigns',
toState: frontendURL(`accounts/${accountId}/campaigns/live_chat`),
toStateName: 'campaigns_livechat_index',
},
{
key: 'oneOffCampaigns',
icon: 'sound-source',
label: 'ONE_OFF',
label: 'SMS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
toStateName: 'one_off',
toState: frontendURL(`accounts/${accountId}/campaigns/sms`),
toStateName: 'campaigns_sms_index',
},
],
});
@@ -3,18 +3,20 @@ import { frontendURL } from '../../../../helper/URLHelper';
const contacts = accountId => ({
parentNav: 'contacts',
routes: [
'contacts_dashboard',
'contact_profile_dashboard',
'contacts_segments_dashboard',
'contacts_labels_dashboard',
'contacts_dashboard_index',
'contacts_dashboard_segments_index',
'contacts_dashboard_labels_index',
'contacts_edit',
'contacts_edit_segment',
'contacts_edit_label',
],
menuItems: [
{
icon: 'contact-card-group',
label: 'ALL_CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
toState: frontendURL(`accounts/${accountId}/contacts?page=1`),
toStateName: 'contacts_dashboard_index',
},
],
});
@@ -22,7 +22,7 @@ const primaryMenuItems = accountId => [
key: 'captain',
label: 'CAPTAIN',
featureFlag: FEATURE_FLAGS.CAPTAIN,
toState: frontendURL(`accounts/${accountId}/captain`),
toState: frontendURL(`accounts/${accountId}/captain/documents`),
toStateName: 'captain',
},
{
@@ -31,7 +31,7 @@ const primaryMenuItems = accountId => [
label: 'CONTACTS',
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
toStateName: 'contacts_dashboard_index',
},
{
icon: 'arrow-trending-lines',
@@ -47,7 +47,7 @@ const primaryMenuItems = accountId => [
label: 'CAMPAIGNS',
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'ongoing_campaigns',
toStateName: 'campaigns_ongoing_index',
},
{
icon: 'library',
@@ -55,8 +55,8 @@ const primaryMenuItems = accountId => [
label: 'HELP_CENTER.TITLE',
featureFlag: FEATURE_FLAGS.HELP_CENTER,
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
toState: frontendURL(`accounts/${accountId}/portals/portal_articles_index`),
toStateName: 'portals_index',
},
{
icon: 'settings',
@@ -40,6 +40,7 @@ const settings = accountId => ({
'settings_teams_list',
'settings_teams_new',
'sla_list',
'custom_roles_list',
],
menuItems: [
{
@@ -179,6 +180,18 @@ const settings = accountId => ({
isEnterpriseOnly: true,
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
},
{
icon: 'scan-person',
label: 'CUSTOM_ROLES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
toStateName: 'custom_roles_list',
isEnterpriseOnly: true,
beta: true,
},
{
icon: 'document-list-clock',
label: 'SLA',
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
export default {
emits: ['toggleAccounts'],
data() {
return { showSwitchButton: false };
},
@@ -25,6 +26,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div
v-if="showShowCurrentAccountContext"
@@ -7,7 +7,7 @@ export default {
default: true,
},
},
emits: ['closeAccountModal', 'showCreateAccountModal'],
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -52,9 +52,13 @@ export default {
{{ account.name }}
</div>
<div
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
>
{{ account.role }}
{{
account.custom_role_id
? account.custom_role.name
: account.role
}}
</div>
</label>
</span>
@@ -1,7 +1,7 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables';
export default {
@@ -15,6 +15,7 @@ export default {
default: true,
},
},
emits: ['closeAccountCreateModal'],
setup() {
return { v$: useVuelidate() };
},
@@ -23,11 +24,13 @@ export default {
accountName: '',
};
},
validations: {
accountName: {
required,
minLength: minLength(1),
},
validations() {
return {
accountName: {
required,
minLength: minLength(1),
},
};
},
computed: {
...mapGetters({
@@ -76,7 +79,7 @@ export default {
<label :class="{ error: v$.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input
v-model.trim="accountName"
v-model="accountName"
type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch"
@@ -6,6 +6,7 @@ export default {
components: {
Thumbnail,
},
emits: ['toggleMenu'],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -2,6 +2,8 @@
import { mapGetters } from 'vuex';
export default {
emits: ['openNotificationPanel'],
computed: {
...mapGetters({
notificationMetadata: 'notifications/getMeta',
@@ -17,6 +17,12 @@ export default {
default: false,
},
},
emits: [
'close',
'openKeyShortcutModal',
'toggleAccounts',
'showSupportChatWindow',
],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -7,6 +7,7 @@ import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
export default {
components: {
@@ -42,6 +43,7 @@ export default {
default: '',
},
},
emits: ['toggleAccounts', 'openNotificationPanel', 'openKeyShortcutModal'],
data() {
return {
helpDocsURL: wootConstants.DOCS_URL,
@@ -60,7 +62,7 @@ export default {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
useTrack(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
this.$emit('openNotificationPanel');
},
},
@@ -80,6 +82,7 @@ export default {
/>
<PrimaryNavItem
v-for="menuItem in menuItems"
:id="menuItem.key"
:key="menuItem.toState"
:icon="menuItem.icon"
:name="menuItem.label"
@@ -88,20 +91,24 @@ export default {
/>
</div>
<div class="flex flex-col items-center justify-end pb-6">
<PrimaryNavItem
<a
v-if="!isACustomBrandedInstance"
icon="book-open-globe"
name="DOCS"
open-in-new-page
:to="helpDocsURL"
/>
<NotificationBell @openNotificationPanel="openNotificationPanel" />
<AgentDetails @toggleMenu="toggleOptions" />
v-tooltip.right="$t(`SIDEBAR.DOCS`)"
:href="helpDocsURL"
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
rel="noopener noreferrer nofollow"
target="_blank"
>
<fluent-icon icon="book-open-globe" />
<span class="sr-only">{{ $t(`SIDEBAR.DOCS`) }}</span>
</a>
<NotificationBell @open-notification-panel="openNotificationPanel" />
<AgentDetails @toggle-menu="toggleOptions" />
<OptionsMenu
:show="showOptionsMenu"
@toggleAccounts="toggleAccountModal"
@showSupportChatWindow="toggleSupportChatWindow"
@openKeyShortcutModal="$emit('openKeyShortcutModal')"
@toggle-accounts="toggleAccountModal"
@show-support-chat-window="toggleSupportChatWindow"
@open-key-shortcut-modal="$emit('openKeyShortcutModal')"
@close="toggleOptions"
/>
</div>
@@ -1,10 +1,23 @@
<script>
import { OnClickOutside } from '@vueuse/components';
import { HELP_CENTER_MENU_ITEMS } from 'dashboard/helper/portalHelper';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
export default {
components: {
DropdownMenu,
OnClickOutside,
},
props: {
to: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
name: {
type: String,
default: '',
@@ -26,15 +39,89 @@ export default {
default: false,
},
},
data() {
return {
helpCenterMenu: HELP_CENTER_MENU_ITEMS,
showHelpCenterMenu: false,
};
},
computed: {
helpCenterMenuItems() {
return this.helpCenterMenu.map(item => ({
...item,
isSelected: this.isSelectedMenuItem(item),
}));
},
isHelpCenter() {
return this.id === 'helpcenter';
},
isHelpCenterSelected() {
const routes = [
'portals_new',
'portals_index',
'portals_articles_index',
'portals_articles_new',
'portals_articles_edit',
'portals_categories_index',
'portals_categories_articles_index',
'portals_categories_articles_edit',
'portals_locales_index',
'portals_settings_index',
];
return routes.includes(this.$route.name);
},
},
methods: {
isSelectedMenuItem(menuItem) {
return menuItem.value.includes(this.$route.name);
},
toggleHelpCenterMenu() {
this.showHelpCenterMenu = !this.showHelpCenterMenu;
},
handleHelpCenterAction({ action }) {
this.$router.push({
name: 'portals_index',
params: {
navigationPath: action,
},
});
},
},
};
</script>
<template>
<router-link v-slot="{ href, isActive, navigate }" :to="to" custom>
<OnClickOutside v-if="isHelpCenter" @trigger="showHelpCenterMenu = false">
<button
v-tooltip.top="$t(`SIDEBAR.${name}`)"
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:!bg-slate-25 dark:hover:!bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isHelpCenterSelected,
}"
@click="toggleHelpCenterMenu"
>
<fluent-icon
:icon="icon"
:class="{
'text-woot-500': isHelpCenterSelected,
}"
/>
<DropdownMenu
v-if="showHelpCenterMenu && isHelpCenter"
:menu-items="helpCenterMenuItems"
class="ltr:left-10 rtl:right-10 w-36 z-[100] top-0 overflow-y-auto max-h-52"
@action="handleHelpCenterAction"
/>
</button>
</OnClickOutside>
<router-link v-else v-slot="{ href, isActive, navigate }" :to="to" custom>
<a
v-tooltip.right="$t(`SIDEBAR.${name}`)"
:href="href"
class="text-slate-700 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isActive || isChildMenuActive,
@@ -52,7 +139,7 @@ export default {
<span class="sr-only">{{ name }}</span>
<span
v-if="count"
class="text-black-900 bg-yellow-500 absolute -top-1 -right-1"
class="absolute bg-yellow-500 text-black-900 -top-1 -right-1"
>
{{ count }}
</span>
@@ -4,7 +4,10 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
import AccountContext from './AccountContext.vue';
import { mapGetters } from 'vuex';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { hasPermissions } from '../../../helper/permissionsHelper';
import {
getUserPermissions,
hasPermissions,
} from '../../../helper/permissionsHelper';
import { routesWithPermissions } from '../../../routes';
export default {
@@ -46,20 +49,21 @@ export default {
default: false,
},
},
emits: ['addLabel', 'toggleAccounts'],
computed: {
...mapGetters({
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
hasSecondaryMenu() {
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
},
contactCustomViews() {
return this.customViews.filter(view => view.filter_type === 'contact');
},
accessibleMenuItems() {
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
menuItem => {
const { permissions: userPermissions = [] } = this.currentUser;
const userPermissions = getUserPermissions(
this.currentUser,
this.accountId
);
return hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
@@ -130,7 +134,7 @@ export default {
icon: 'number-symbol',
label: 'TAGGED_WITH',
hasSubMenu: true,
key: 'label',
key: 'labels',
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
newLinkTag: 'NEW_LABEL',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
@@ -143,7 +147,7 @@ export default {
color: label.color,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/labels/${label.title}/contacts`
`accounts/${this.accountId}/contacts/labels/${label.title}`
),
})),
};
@@ -190,7 +194,7 @@ export default {
icon: 'folder',
label: 'CUSTOM_VIEWS_SEGMENTS',
hasSubMenu: true,
key: 'custom_view',
key: 'segments',
children: this.customViews
.filter(view => view.filter_type === 'contact')
.map(view => ({
@@ -198,7 +202,7 @@ export default {
label: view.name,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/contacts/custom_view/${view.id}`
`accounts/${this.accountId}/contacts/segments/${view.id}`
),
})),
};
@@ -237,14 +241,13 @@ export default {
<template>
<div
v-if="hasSecondaryMenu"
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
>
<AccountContext @toggleAccounts="toggleAccountModal" />
<AccountContext @toggle-accounts="toggleAccountModal" />
<transition-group
name="menu-list"
tag="ul"
class="pt-2 mb-0 ml-0 list-none"
class="pt-2 list-none reset-base"
>
<SecondaryNavItem
v-for="menuItem in accessibleMenuItems"
@@ -255,7 +258,7 @@ export default {
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key"
:menu-item="menuItem"
@addLabel="showAddLabelPopup"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
@@ -58,7 +58,7 @@ export default {
active-class="active"
>
<li
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
class="h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
@@ -105,7 +105,7 @@ export default {
</span>
<span
v-if="showChildCount"
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs font-medium mx-1 py-0 px-1"
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1"
:class="
isCountZero
? `text-slate-300 dark:text-slate-500`
@@ -22,6 +22,7 @@ export default {
default: () => ({}),
},
},
emits: ['addLabel', 'open'],
setup() {
const { isAdmin } = useAdmin();
const { isEnterprise } = useConfig();
@@ -102,7 +103,7 @@ export default {
},
isInboxSettings() {
return (
this.$store.state.route.name === 'settings_inbox_show' &&
this.$route.name === 'settings_inbox_show' &&
this.menuItem.toStateName === 'settings_inbox_list'
);
},
@@ -118,6 +119,13 @@ export default {
this.menuItem.toStateName === 'settings_applications'
);
},
isContactsDefaultRoute() {
return (
this.menuItem.toStateName === 'contacts_dashboard_index' &&
(this.$store.state.route.name === 'contacts_dashboard_index' ||
this.$store.state.route.name === 'contacts_edit')
);
},
isCurrentRoute() {
return this.$store.state.route.name.includes(this.menuItem.toStateName);
},
@@ -129,6 +137,7 @@ export default {
this.isAllConversations ||
this.isMentions ||
this.isUnattended ||
this.isContactsDefaultRoute ||
this.isCurrentRoute
) {
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
@@ -208,7 +217,7 @@ export default {
</div>
<router-link
v-else
class="flex items-center p-2 m-0 text-sm font-medium leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
class="flex items-center p-2 m-0 text-sm leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
:class="computedClass"
:to="menuItem && menuItem.toState"
>
@@ -220,7 +229,7 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
v-if="showChildCount(menuItem.count)"
class="px-1 py-0 mx-1 font-medium rounded-md text-xxs"
class="px-1 py-0 mx-1 rounded-md text-xxs"
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
@@ -235,13 +244,13 @@ export default {
v-if="menuItem.beta"
data-view-component="true"
label="Beta"
class="inline-block px-1 mx-1 font-medium leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
class="inline-block px-1 mx-1 leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
>
{{ $t('SIDEBAR.BETA') }}
</span>
</router-link>
<ul v-if="hasSubMenu" class="mb-0 ml-0 list-none">
<ul v-if="hasSubMenu" class="list-none reset-base">
<SecondaryChildNavItem
v-for="child in menuItem.children"
:key="child.id"
@@ -1,79 +1,62 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AccountSelector from '../AccountSelector.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const localVue = createLocalVue();
localVue.component('woot-modal', WootModal);
localVue.component('woot-modal-header', WootModalHeader);
localVue.component('fluent-icon', FluentIcon);
localVue.use(Vuex);
localVue.use(VueI18n);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
const store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentAccountId: () => 1,
getCurrentUser: () => ({
accounts: [
{ id: 1, name: 'Chatwoot', role: 'administrator' },
{ id: 2, name: 'GitX', role: 'agent' },
],
}),
},
},
globalConfig: {
namespaced: true,
getters: {
get: () => ({ createNewAccountFromDashboard: false }),
},
},
},
});
describe('accountSelctor', () => {
describe('AccountSelector', () => {
let accountSelector = null;
const currentUser = {
accounts: [
{
id: 1,
name: 'Chatwoot',
role: 'administrator',
},
{
id: 2,
name: 'GitX',
role: 'agent',
},
],
};
let actions = null;
let modules = null;
beforeEach(() => {
actions = {};
modules = {
auth: {
getters: {
getCurrentAccountId: () => 1,
getCurrentUser: () => currentUser,
},
},
globalConfig: {
getters: {
'globalConfig/get': () => ({ createNewAccountFromDashboard: false }),
},
},
};
let store = new Vuex.Store({ actions, modules });
accountSelector = mount(AccountSelector, {
store,
localVue,
i18n: i18nConfig,
propsData: { showAccountModal: true },
stubs: { WootButton: { template: '<button />' } },
global: {
plugins: [store],
components: {
'woot-modal': WootModal,
'woot-modal-header': WootModalHeader,
'fluent-icon': FluentIcon,
},
stubs: {
WootButton: { template: '<button />' },
// override global stub
WootModalHeader: false,
},
},
props: { showAccountModal: true },
});
});
it('title and sub title exist', () => {
const headerComponent = accountSelector.findComponent(WootModalHeader);
const title = headerComponent.findComponent({ ref: 'modalHeaderTitle' });
expect(title.text()).toBe('Switch Account');
const content = headerComponent.findComponent({
ref: 'modalHeaderContent',
});
const title = headerComponent.find('[data-test-id="modal-header-title"]');
expect(title.text()).toBe('Switch account');
const content = headerComponent.find(
'[data-test-id="modal-header-content"]'
);
expect(content.text()).toBe('Select an account from the following list');
});
@@ -1,28 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('thumbnail', Thumbnail);
localVue.component('woot-button', WootButton);
localVue.component('woot-button', WootButton);
localVue.use(VTooltip, {
defaultHtml: false,
});
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
describe('agentDetails', () => {
describe('AgentDetails', () => {
const currentUser = {
name: 'Neymar Junior',
avatar_url: '',
@@ -30,37 +12,46 @@ describe('agentDetails', () => {
};
const currentRole = 'agent';
let store = null;
let actions = null;
let modules = null;
let agentDetails = null;
beforeEach(() => {
actions = {};
const mockTooltipDirective = {
mounted: (el, binding) => {
// You can mock the behavior here if necessary
el.setAttribute('data-tooltip', binding.value || '');
},
};
modules = {
auth: {
getters: {
getCurrentUser: () => currentUser,
getCurrentRole: () => currentRole,
getCurrentUserAvailability: () => currentUser.availability_status,
beforeEach(() => {
store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentUser: () => currentUser,
getCurrentRole: () => currentRole,
getCurrentUserAvailability: () => currentUser.availability_status,
},
},
},
};
store = new Vuex.Store({
actions,
modules,
});
agentDetails = shallowMount(AgentDetails, {
store,
localVue,
i18n: i18nConfig,
global: {
plugins: [store],
components: {
Thumbnail,
WootButton,
},
directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive
},
stubs: { WootButton: { template: '<button><slot /></button>' } },
},
});
});
it(' the agent status', () => {
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
it('shows the correct agent status', () => {
expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
});
it('agent thumbnail exists', () => {
@@ -1,20 +1,7 @@
import NotificationBell from '../NotificationBell.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
import NotificationBell from '../NotificationBell.vue';
const $route = {
name: 'notifications_index',
@@ -33,43 +20,51 @@ describe('notificationBell', () => {
};
modules = {
auth: {
namespaced: false,
getters: {
getCurrentAccountId: () => accountId,
},
},
notifications: {
namespaced: false,
getters: {
'notifications/getMeta': () => notificationMetadata,
},
},
};
store = new Vuex.Store({
store = createStore({
actions,
modules,
});
});
it('it should return unread count 19 ', () => {
it('it should return unread count 19', () => {
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
},
});
expect(wrapper.vm.unreadCount).toBe('19');
});
it('it should return unread count 99+ ', async () => {
it('it should return unread count 99+', async () => {
notificationMetadata.unreadCount = 100;
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
},
});
expect(wrapper.vm.unreadCount).toBe('99+');
@@ -77,11 +72,14 @@ describe('notificationBell', () => {
it('isNotificationPanelActive', async () => {
const notificationBell = shallowMount(NotificationBell, {
store,
localVue,
i18n: i18nConfig,
mocks: {
$route,
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
},
});
@@ -1,9 +1,6 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -11,70 +8,64 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(VTooltip, {
defaultHtml: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('woot-button', WootButton);
localVue.component('woot-dropdown-header', WootDropdownHeader);
localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => {
const currentAvailability = 'online';
const currentAccountId = '1';
const currentUserAutoOffline = false;
let store = null;
let actions = null;
let modules = null;
let availabilityStatus = null;
beforeEach(() => {
actions = {
updateAvailability: vi.fn(() => {
return Promise.resolve();
}),
updateAvailability: vi.fn(() => Promise.resolve()),
};
modules = {
auth: {
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
},
},
};
store = new Vuex.Store({ actions, modules });
availabilityStatus = mount(AvailabilityStatus, {
store,
localVue,
i18n: i18nConfig,
stubs: { WootSwitch: { template: '<button />' } },
actions,
});
});
it('dispatches an action when user changes status', async () => {
await availabilityStatus;
availabilityStatus
.findAll('.status-change--dropdown-button')
.at(2)
.trigger('click');
const wrapper = mount(AvailabilityStatus, {
global: {
plugins: [store],
components: {
WootButton,
WootDropdownItem,
WootDropdownMenu,
WootDropdownHeader,
WootDropdownDivider,
FluentIcon,
},
stubs: {
WootSwitch: { template: '<button />' },
},
},
});
expect(actions.updateAvailability).toBeCalledWith(
expect.any(Object),
{ availability: 'offline', account_id: currentAccountId },
undefined
);
// Ensure that the dropdown menu is opened
await wrapper.vm.openStatusMenu();
// Simulate the user clicking the 3rd button (offline status)
const buttons = wrapper.findAll('.status-change--dropdown-button');
expect(buttons.length).toBeGreaterThan(0); // Ensure buttons exist
await buttons[2].trigger('click');
expect(actions.updateAvailability).toHaveBeenCalledTimes(1);
expect(actions.updateAvailability.mock.calls[0][1]).toEqual({
availability: 'offline',
account_id: currentAccountId,
});
});
});