feat: Add dropdown component (#10358)
This PR adds dropdown primitives to help compose custom dropdowns across the app. The following the sample usage --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -31,7 +31,13 @@ hr {
|
|||||||
ul,
|
ul,
|
||||||
ol,
|
ol,
|
||||||
dl {
|
dl {
|
||||||
@apply mb-2 list-disc list-outside leading-[1.65];
|
@apply list-disc list-outside leading-[1.65];
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:not(.reset-base),
|
||||||
|
ol:not(.reset-base),
|
||||||
|
dl:not(.reset-base) {
|
||||||
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form elements
|
// Form elements
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @TODDO - Remove after moving all buttons to woot-button
|
// @TODDO - Remove after moving all buttons to woot-button
|
||||||
.icon+.button__content {
|
.icon + .button__content {
|
||||||
@apply w-auto;
|
@apply w-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import DropdownContainer from './base/DropdownContainer.vue';
|
||||||
|
import DropdownBody from './base/DropdownBody.vue';
|
||||||
|
import DropdownSection from './base/DropdownSection.vue';
|
||||||
|
import DropdownItem from './base/DropdownItem.vue';
|
||||||
|
import DropdownSeparator from './base/DropdownSeparator.vue';
|
||||||
|
import WootSwitch from 'components/ui/Switch.vue';
|
||||||
|
|
||||||
|
const currentUserAutoOffline = ref(false);
|
||||||
|
|
||||||
|
const menuItems = ref([
|
||||||
|
{
|
||||||
|
label: 'Contact Support',
|
||||||
|
icon: 'i-lucide-life-buoy',
|
||||||
|
click: () => window.alert('Contact Support'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Keyboard Shortcuts',
|
||||||
|
icon: 'i-lucide-keyboard',
|
||||||
|
click: () => window.alert('Keyboard Shortcuts'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Profile Settings',
|
||||||
|
icon: 'i-lucide-user-pen',
|
||||||
|
click: () => window.alert('Profile Settings'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Change Appearance',
|
||||||
|
icon: 'i-lucide-swatch-book',
|
||||||
|
click: () => window.alert('Change Appearance'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open SuperAdmin',
|
||||||
|
icon: 'i-lucide-castle',
|
||||||
|
link: '/super_admin',
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log Out',
|
||||||
|
icon: 'i-lucide-log-out',
|
||||||
|
click: () => window.alert('Log Out'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/DropdownPrimitives"
|
||||||
|
:layout="{ type: 'grid', width: 400, height: 800 }"
|
||||||
|
>
|
||||||
|
<Variant title="Profile Menu">
|
||||||
|
<div class="p-4 bg-white h-[500px] dark:bg-slate-900">
|
||||||
|
<DropdownContainer>
|
||||||
|
<template #trigger="{ toggle }">
|
||||||
|
<Button label="Open Menu" size="sm" @click="toggle" />
|
||||||
|
</template>
|
||||||
|
<DropdownBody class="w-80">
|
||||||
|
<DropdownSection title="Profile Options">
|
||||||
|
<DropdownItem label="Contact Support" class="justify-between">
|
||||||
|
<span>{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}</span>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<WootSwitch v-model="currentUserAutoOffline" />
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
<DropdownSeparator />
|
||||||
|
<DropdownItem
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.label"
|
||||||
|
v-bind="item"
|
||||||
|
/>
|
||||||
|
</DropdownBody>
|
||||||
|
</DropdownContainer>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute">
|
||||||
|
<ul
|
||||||
|
class="text-sm bg-n-solid-1 border border-n-weak rounded-xl shadow-sm py-2 n-dropdown-body gap-2 grid list-none px-2 reset-base"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { provideDropdownContext } from './provider.js';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const [isOpen, toggle] = useToggle(false);
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
emit('close');
|
||||||
|
toggle(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
provideDropdownContext({
|
||||||
|
isOpen,
|
||||||
|
toggle,
|
||||||
|
closeMenu,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative z-20 space-y-2">
|
||||||
|
<slot name="trigger" :is-open :toggle="() => toggle()" />
|
||||||
|
<div v-if="isOpen" v-on-clickaway="closeMenu" class="absolute">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import { useDropdownContext } from './provider.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
icon: { type: [String, Object, Function], default: '' },
|
||||||
|
link: { type: String, default: '' },
|
||||||
|
click: { type: Function, default: null },
|
||||||
|
preserveOpen: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { closeMenu } = useDropdownContext();
|
||||||
|
|
||||||
|
const componentIs = computed(() => {
|
||||||
|
if (props.link) return 'router-link';
|
||||||
|
if (props.click) return 'button';
|
||||||
|
|
||||||
|
return 'div';
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerClick = () => {
|
||||||
|
if (props.click) {
|
||||||
|
props.click();
|
||||||
|
if (!props.preserveOpen) closeMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="n-dropdown-item">
|
||||||
|
<component
|
||||||
|
:is="componentIs"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="flex text-left rtl:text-right items-center p-2 reset-base text-sm text-n-slate-12 w-full border-0"
|
||||||
|
:class="{
|
||||||
|
'hover:bg-n-alpha-1 rounded-lg w-full gap-3': !$slots.default,
|
||||||
|
}"
|
||||||
|
:href="props.link || null"
|
||||||
|
@click="triggerClick"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<slot name="icon">
|
||||||
|
<Icon v-if="icon" class="size-4 text-n-slate-11" :icon="icon" />
|
||||||
|
</slot>
|
||||||
|
<slot name="label">{{ label }}</slot>
|
||||||
|
</slot>
|
||||||
|
</component>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="-mx-2 n-dropdown-section">
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
class="px-4 mb-3 mt-1 leading-4 font-medium tracking-[0.2px] text-n-slate-10 text-xs"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<ul class="gap-2 grid reset-base list-none px-2">
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-0 border-b border-n-strong -mx-2" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import DropdownBody from './DropdownBody.vue';
|
||||||
|
import DropdownContainer from './DropdownContainer.vue';
|
||||||
|
import DropdownItem from './DropdownItem.vue';
|
||||||
|
import DropdownSection from './DropdownSection.vue';
|
||||||
|
import DropdownSeparator from './DropdownSeparator.vue';
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownBody,
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownSection,
|
||||||
|
DropdownSeparator,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { inject, provide } from 'vue';
|
||||||
|
|
||||||
|
const DropdownControl = Symbol('DropdownControl');
|
||||||
|
|
||||||
|
export function useDropdownContext() {
|
||||||
|
const context = inject(DropdownControl, null);
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error(
|
||||||
|
`Component is missing a parent <DropdownContainer /> component.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provideDropdownContext(context) {
|
||||||
|
provide(DropdownControl, context);
|
||||||
|
}
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useToggle } from '@vueuse/core';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import ButtonNext from 'next/button/Button.vue';
|
import ButtonNext from 'next/button/Button.vue';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownBody,
|
||||||
|
DropdownSection,
|
||||||
|
DropdownItem,
|
||||||
|
} from 'next/dropdown-menu/base';
|
||||||
|
|
||||||
const emit = defineEmits(['showCreateAccountModal']);
|
const emit = defineEmits(['showCreateAccountModal']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { accountId, currentAccount } = useAccount();
|
const { accountId, currentAccount } = useAccount();
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
const globalConfig = useMapGetter('globalConfig/get');
|
const globalConfig = useMapGetter('globalConfig/get');
|
||||||
const [showDropdown, toggleDropdown] = useToggle(false);
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
if (showDropdown.value) {
|
|
||||||
toggleDropdown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeAccount = newId => {
|
const onChangeAccount = newId => {
|
||||||
const accountUrl = `/app/accounts/${newId}/dashboard`;
|
const accountUrl = `/app/accounts/${newId}/dashboard`;
|
||||||
@@ -26,51 +25,45 @@ const onChangeAccount = newId => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const emitNewAccount = () => {
|
const emitNewAccount = () => {
|
||||||
close();
|
|
||||||
emit('showCreateAccountModal');
|
emit('showCreateAccountModal');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative z-20">
|
<DropdownContainer>
|
||||||
<button
|
<template #trigger="{ toggle, isOpen }">
|
||||||
id="sidebar-account-switcher"
|
<button
|
||||||
:data-account-id="accountId"
|
id="sidebar-account-switcher"
|
||||||
aria-haspopup="listbox"
|
:data-account-id="accountId"
|
||||||
aria-controls="account-options"
|
aria-haspopup="listbox"
|
||||||
class="flex items-center gap-2 justify-between w-full rounded-lg hover:bg-n-alpha-1 px-2"
|
aria-controls="account-options"
|
||||||
:class="{ 'bg-n-alpha-1': showDropdown }"
|
class="flex items-center gap-2 justify-between w-full rounded-lg hover:bg-n-alpha-1 px-2"
|
||||||
@click="toggleDropdown()"
|
:class="{ 'bg-n-alpha-1': isOpen }"
|
||||||
>
|
@click="toggle"
|
||||||
<span
|
|
||||||
class="text-sm font-medium leading-5 text-n-slate-12 truncate"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
>
|
||||||
{{ currentAccount.name }}
|
<span
|
||||||
</span>
|
class="text-sm font-medium leading-5 text-n-slate-12 truncate"
|
||||||
|
aria-live="polite"
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="i-lucide-chevron-down size-4 text-n-slate-10 flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div v-if="showDropdown" v-on-clickaway="close" class="absolute top-8 z-50">
|
|
||||||
<div
|
|
||||||
class="min-w-72 max-w-96 text-sm bg-n-solid-1 border border-n-weak rounded-xl shadow-sm py-4 px-2 flex flex-col gap-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="px-4 leading-4 font-medium tracking-[0.2px] text-n-slate-10 text-xs"
|
|
||||||
>
|
>
|
||||||
{{ t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
{{ currentAccount.name }}
|
||||||
</div>
|
</span>
|
||||||
<div class="px-1 gap-1 grid">
|
|
||||||
<button
|
<span
|
||||||
v-for="account in currentUser.accounts"
|
aria-hidden="true"
|
||||||
:id="`account-${account.id}`"
|
class="i-lucide-chevron-down size-4 text-n-slate-10 flex-shrink-0"
|
||||||
:key="account.id"
|
/>
|
||||||
class="flex w-full hover:bg-n-alpha-1 space-x-4"
|
</button>
|
||||||
@click="onChangeAccount(account.id)"
|
</template>
|
||||||
>
|
<DropdownBody class="min-w-80">
|
||||||
|
<DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_WORKSPACE')">
|
||||||
|
<DropdownItem
|
||||||
|
v-for="account in currentUser.accounts"
|
||||||
|
:id="`account-${account.id}`"
|
||||||
|
:key="account.id"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="onChangeAccount(account.id)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
<div
|
<div
|
||||||
:for="account.name"
|
:for="account.name"
|
||||||
class="text-left rtl:text-right flex gap-2 items-center"
|
class="text-left rtl:text-right flex gap-2 items-center"
|
||||||
@@ -98,19 +91,20 @@ const emitNewAccount = () => {
|
|||||||
icon="i-lucide-check"
|
icon="i-lucide-check"
|
||||||
class="text-n-teal-11 size-5"
|
class="text-n-teal-11 size-5"
|
||||||
/>
|
/>
|
||||||
</button>
|
</template>
|
||||||
</div>
|
</DropdownItem>
|
||||||
<div v-if="globalConfig.createNewAccountFromDashboard" class="px-2">
|
</DropdownSection>
|
||||||
<ButtonNext
|
<DropdownItem v-if="globalConfig.createNewAccountFromDashboard">
|
||||||
variant="secondary"
|
<ButtonNext
|
||||||
class="w-full"
|
color="slate"
|
||||||
size="sm"
|
variant="faded"
|
||||||
@click="emitNewAccount"
|
class="w-full"
|
||||||
>
|
size="sm"
|
||||||
{{ t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
@click="emitNewAccount"
|
||||||
</ButtonNext>
|
>
|
||||||
</div>
|
{{ t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||||
</div>
|
</ButtonNext>
|
||||||
</div>
|
</DropdownItem>
|
||||||
</div>
|
</DropdownBody>
|
||||||
|
</DropdownContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const toggleTrigger = () => {
|
|||||||
:permissions="resolvePermissions(to)"
|
:permissions="resolvePermissions(to)"
|
||||||
:feature-flag="resolveFeatureFlag(to)"
|
:feature-flag="resolveFeatureFlag(to)"
|
||||||
as="li"
|
as="li"
|
||||||
class="text-sm cursor-pointer select-none gap-1 grid"
|
class="grid gap-1 text-sm cursor-pointer select-none"
|
||||||
>
|
>
|
||||||
<SidebarGroupHeader
|
<SidebarGroupHeader
|
||||||
:icon
|
:icon
|
||||||
@@ -117,7 +117,7 @@ const toggleTrigger = () => {
|
|||||||
<ul
|
<ul
|
||||||
v-if="hasChildren"
|
v-if="hasChildren"
|
||||||
v-show="isExpanded || hasActiveChild"
|
v-show="isExpanded || hasActiveChild"
|
||||||
class="list-none m-0 grid sidebar-group-children"
|
class="grid m-0 list-none sidebar-group-children"
|
||||||
>
|
>
|
||||||
<template v-for="child in children" :key="child.name">
|
<template v-for="child in children" :key="child.name">
|
||||||
<SidebarSubGroup
|
<SidebarSubGroup
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import Auth from 'dashboard/api/auth';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useToggle } from '@vueuse/core';
|
|
||||||
import Avatar from 'next/avatar/Avatar.vue';
|
import Avatar from 'next/avatar/Avatar.vue';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
|
||||||
import SidebarProfileMenuStatus from './SidebarProfileMenuStatus.vue';
|
import SidebarProfileMenuStatus from './SidebarProfileMenuStatus.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownBody,
|
||||||
|
DropdownSeparator,
|
||||||
|
DropdownItem,
|
||||||
|
} from 'next/dropdown-menu/base';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'openKeyShortcutModal']);
|
const emit = defineEmits(['close', 'openKeyShortcutModal']);
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -21,14 +26,6 @@ const router = useRouter();
|
|||||||
const globalConfig = useMapGetter('globalConfig/get');
|
const globalConfig = useMapGetter('globalConfig/get');
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
||||||
const [showProfileMenu, toggleProfileMenu] = useToggle(false);
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
if (showProfileMenu.value) {
|
|
||||||
emit('close');
|
|
||||||
toggleProfileMenu(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
return [
|
return [
|
||||||
@@ -37,7 +34,6 @@ const menuItems = computed(() => {
|
|||||||
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
|
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
|
||||||
icon: 'i-lucide-life-buoy',
|
icon: 'i-lucide-life-buoy',
|
||||||
click: () => {
|
click: () => {
|
||||||
closeMenu();
|
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -46,7 +42,6 @@ const menuItems = computed(() => {
|
|||||||
label: t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS'),
|
label: t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS'),
|
||||||
icon: 'i-lucide-keyboard',
|
icon: 'i-lucide-keyboard',
|
||||||
click: () => {
|
click: () => {
|
||||||
closeMenu();
|
|
||||||
emit('openKeyShortcutModal');
|
emit('openKeyShortcutModal');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -55,20 +50,26 @@ const menuItems = computed(() => {
|
|||||||
label: t('SIDEBAR_ITEMS.PROFILE_SETTINGS'),
|
label: t('SIDEBAR_ITEMS.PROFILE_SETTINGS'),
|
||||||
icon: 'i-lucide-user-pen',
|
icon: 'i-lucide-user-pen',
|
||||||
click: () => {
|
click: () => {
|
||||||
closeMenu();
|
|
||||||
router.push({ name: 'profile_settings_index' });
|
router.push({ name: 'profile_settings_index' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: true,
|
show: true,
|
||||||
label: t('SIDEBAR_ITEMS.APPEARANCE'),
|
label: t('SIDEBAR_ITEMS.APPEARANCE'),
|
||||||
icon: 'i-lucide-swatch-book',
|
icon: 'i-lucide-palette',
|
||||||
click: () => {
|
click: () => {
|
||||||
closeMenu();
|
|
||||||
const ninja = document.querySelector('ninja-keys');
|
const ninja = document.querySelector('ninja-keys');
|
||||||
ninja.open({ parent: 'appearance_settings' });
|
ninja.open({ parent: 'appearance_settings' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
label: t('SIDEBAR_ITEMS.DOCS'),
|
||||||
|
icon: 'i-lucide-book',
|
||||||
|
click: () => {
|
||||||
|
window.open('https://www.chatwoot.com/hc/user-guide/en', '_blank');
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
show: currentUser.value.type === 'SuperAdmin',
|
show: currentUser.value.type === 'SuperAdmin',
|
||||||
label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'),
|
label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'),
|
||||||
@@ -79,7 +80,7 @@ const menuItems = computed(() => {
|
|||||||
{
|
{
|
||||||
show: true,
|
show: true,
|
||||||
label: t('SIDEBAR_ITEMS.LOGOUT'),
|
label: t('SIDEBAR_ITEMS.LOGOUT'),
|
||||||
icon: 'i-lucide-log-out',
|
icon: 'i-lucide-power',
|
||||||
click: Auth.logout,
|
click: Auth.logout,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -91,56 +92,40 @@ const allowedMenuItems = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative z-20 w-full min-w-0">
|
<DropdownContainer
|
||||||
<button
|
class="relative z-20 w-full min-w-0"
|
||||||
class="flex gap-2 items-center rounded-lg cursor-pointer text-left w-full hover:bg-n-alpha-1 p-1"
|
@close="emit('close')"
|
||||||
v-bind="$attrs"
|
>
|
||||||
:class="{
|
<template #trigger="{ toggle, isOpen }">
|
||||||
'bg-n-alpha-1': showProfileMenu,
|
<button
|
||||||
}"
|
class="flex gap-2 items-center rounded-lg cursor-pointer text-left w-full hover:bg-n-alpha-1 p-1"
|
||||||
@click="toggleProfileMenu"
|
:class="{ 'bg-n-alpha-1': isOpen }"
|
||||||
>
|
@click="toggle"
|
||||||
<Avatar
|
|
||||||
:size="32"
|
|
||||||
:name="currentUser.available_name"
|
|
||||||
:src="currentUser.avatar_url"
|
|
||||||
:status="currentUserAvailability"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
rounded-full
|
|
||||||
/>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<div class="text-n-slate-12 text-sm leading-4 font-medium truncate">
|
|
||||||
{{ currentUser.available_name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-n-slate-11 text-xs truncate">
|
|
||||||
{{ currentUser.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="showProfileMenu"
|
|
||||||
v-on-clickaway="closeMenu"
|
|
||||||
class="absolute left-0 bottom-12 z-50"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-72 min-h-32 bg-n-solid-1 border border-n-weak rounded-xl shadow-sm"
|
|
||||||
>
|
>
|
||||||
<SidebarProfileMenuStatus />
|
<Avatar
|
||||||
<div class="border-t border-n-strong mx-2 my-0" />
|
:size="32"
|
||||||
<ul class="list-none m-0 grid gap-1 p-1 text-n-slate-12">
|
:name="currentUser.available_name"
|
||||||
<li v-for="item in allowedMenuItems" :key="item.label" class="m-0">
|
:src="currentUser.avatar_url"
|
||||||
<component
|
:status="currentUserAvailability"
|
||||||
:is="item.link ? 'a' : 'button'"
|
class="flex-shrink-0"
|
||||||
v-bind="item.link ? { target: item.target, href: item.link } : {}"
|
rounded-full
|
||||||
class="text-left hover:bg-n-alpha-1 px-2 py-1.5 w-full flex items-center gap-2"
|
/>
|
||||||
@click="item.click"
|
<div class="min-w-0">
|
||||||
>
|
<div class="text-n-slate-12 text-sm leading-4 font-medium truncate">
|
||||||
<Icon :icon="item.icon" class="size-4" />
|
{{ currentUser.available_name }}
|
||||||
{{ item.label }}
|
</div>
|
||||||
</component>
|
<div class="text-n-slate-11 text-xs truncate">
|
||||||
</li>
|
{{ currentUser.email }}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
|
<DropdownBody class="left-0 bottom-12 z-50 w-80 mb-1">
|
||||||
|
<SidebarProfileMenuStatus />
|
||||||
|
<DropdownSeparator />
|
||||||
|
<template v-for="item in allowedMenuItems" :key="item.label">
|
||||||
|
<DropdownItem v-if="item.show" v-bind="item" />
|
||||||
|
</template>
|
||||||
|
</DropdownBody>
|
||||||
|
</DropdownContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, h } from 'vue';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownBody,
|
||||||
|
DropdownSection,
|
||||||
|
DropdownItem,
|
||||||
|
} from 'next/dropdown-menu/base';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
|
import Button from 'next/button/Button.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -22,14 +29,22 @@ const statusList = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusColors = ['bg-n-teal-9', 'bg-n-amber-9', 'bg-n-slate-9'];
|
||||||
|
|
||||||
const availabilityStatuses = computed(() => {
|
const availabilityStatuses = computed(() => {
|
||||||
return statusList.value.map((statusLabel, index) => ({
|
return statusList.value.map((statusLabel, index) => ({
|
||||||
label: statusLabel,
|
label: statusLabel,
|
||||||
value: AVAILABILITY_STATUS_KEYS[index],
|
value: AVAILABILITY_STATUS_KEYS[index],
|
||||||
|
color: statusColors[index],
|
||||||
|
icon: h('span', { class: [statusColors[index], 'size-[12px] rounded'] }),
|
||||||
active: currentUserAvailability.value === AVAILABILITY_STATUS_KEYS[index],
|
active: currentUserAvailability.value === AVAILABILITY_STATUS_KEYS[index],
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeStatus = computed(() => {
|
||||||
|
return availabilityStatuses.value.find(status => status.active);
|
||||||
|
});
|
||||||
|
|
||||||
function changeAvailabilityStatus(availability) {
|
function changeAvailabilityStatus(availability) {
|
||||||
try {
|
try {
|
||||||
store.dispatch('updateAvailability', {
|
store.dispatch('updateAvailability', {
|
||||||
@@ -50,70 +65,58 @@ function updateAutoOffline(autoOffline) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pt-2 text-n-slate-12">
|
<DropdownSection>
|
||||||
<span
|
<div class="grid gap-0">
|
||||||
class="px-3 leading-4 font-medium tracking-[0.2px] text-n-slate-10 text-xs"
|
<DropdownItem>
|
||||||
>
|
<div class="flex-grow flex items-center gap-1">
|
||||||
{{ t('SIDEBAR.SET_AVAILABILITY_TITLE') }}
|
{{ $t('SIDEBAR.SET_YOUR_AVAILABILITY') }}
|
||||||
</span>
|
</div>
|
||||||
<ul class="list-none m-0 grid gap-1 p-1">
|
<DropdownContainer>
|
||||||
<li
|
<template #trigger="{ toggle }">
|
||||||
v-for="status in availabilityStatuses"
|
<Button
|
||||||
:key="status.value"
|
size="sm"
|
||||||
class="flex items-baseline"
|
color="slate"
|
||||||
>
|
variant="faded"
|
||||||
<button
|
class="min-w-[96px]"
|
||||||
class="text-left rtl:text-right hover:bg-n-alpha-1 px-2 py-1.5 w-full flex items-center gap-2"
|
icon="i-lucide-chevron-down"
|
||||||
:class="{
|
trailing-icon
|
||||||
'pointer-events-none bg-n-amber-10/10': status.active,
|
@click="toggle"
|
||||||
'bg-n-teal-3': status.active && status.value === 'online',
|
>
|
||||||
'bg-n-amber-3': status.active && status.value === 'busy',
|
<div class="flex gap-1 items-center flex-grow text-sm">
|
||||||
'bg-n-slate-3': status.active && status.value === 'offline',
|
<div class="p-1 flex-shrink-0">
|
||||||
}"
|
<div class="size-2 rounded-sm" :class="activeStatus.color" />
|
||||||
@click="changeAvailabilityStatus(status.value)"
|
</div>
|
||||||
>
|
<span>{{ activeStatus.label }}</span>
|
||||||
<div
|
</div>
|
||||||
class="rounded-full w-2.5 h-2.5"
|
</Button>
|
||||||
:class="{
|
</template>
|
||||||
'bg-n-teal-10': status.value === 'online',
|
<DropdownBody class="min-w-32">
|
||||||
'bg-n-amber-10': status.value === 'busy',
|
<DropdownItem
|
||||||
'bg-n-slate-10': status.value === 'offline',
|
v-for="status in availabilityStatuses"
|
||||||
}"
|
:key="status.value"
|
||||||
/>
|
:label="status.label"
|
||||||
<span class="flex-grow">{{ status.label }}</span>
|
:icon="status.icon"
|
||||||
<Icon
|
class="cursor-pointer"
|
||||||
v-if="status.active"
|
@click="changeAvailabilityStatus(status.value)"
|
||||||
icon="i-lucide-check"
|
/>
|
||||||
class="size-4 flex-shrink-0"
|
</DropdownBody>
|
||||||
:class="{
|
</DropdownContainer>
|
||||||
'text-n-teal-11': status.value === 'online',
|
</DropdownItem>
|
||||||
'text-n-amber-11': status.value === 'busy',
|
<DropdownItem>
|
||||||
'text-n-slate-11': status.value === 'offline',
|
<div class="flex-grow flex items-center gap-1">
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-n-strong mx-2 my-0" />
|
|
||||||
<ul class="list-none m-0 grid gap-1 p-1">
|
|
||||||
<li class="px-2 py-1.5 flex items-start w-full gap-2">
|
|
||||||
<div class="h-5 flex items-center flex-shrink-0">
|
|
||||||
<Icon icon="i-lucide-info" class="size-4" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<div class="h-5 leading-none flex place-items-center text-n-slate-12">
|
|
||||||
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||||
|
<Icon
|
||||||
|
v-tooltip.top="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_SHORT')"
|
||||||
|
icon="i-lucide-info"
|
||||||
|
class="size-4 text-n-slate-10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs leading-tight text-n-slate-10 mt-1">
|
<woot-switch
|
||||||
{{ t('SIDEBAR.SET_AUTO_OFFLINE.INFO_SHORT') }}
|
class="flex-shrink-0"
|
||||||
</div>
|
:model-value="currentUserAutoOffline"
|
||||||
</div>
|
@input="updateAutoOffline"
|
||||||
<woot-switch
|
/>
|
||||||
class="flex-shrink-0"
|
</DropdownItem>
|
||||||
:model-value="currentUserAutoOffline"
|
</div>
|
||||||
@input="updateAutoOffline"
|
</DropdownSection>
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ useEventListener(scrollableContainer, 'scroll', () => {
|
|||||||
:icon
|
:icon
|
||||||
class="my-1"
|
class="my-1"
|
||||||
/>
|
/>
|
||||||
<ul class="m-0 list-none relative group">
|
<ul class="m-0 list-none reset-base relative group">
|
||||||
<!-- Each element has h-8, which is 32px, we will show 7 items with one hidden at the end,
|
<!-- Each element has h-8, which is 32px, we will show 7 items with one hidden at the end,
|
||||||
which is 14rem. Then we add 16px so that we have some text visible from the next item -->
|
which is 14rem. Then we add 16px so that we have some text visible from the next item -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default {
|
|||||||
<transition-group
|
<transition-group
|
||||||
name="menu-list"
|
name="menu-list"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
class="pt-2 mb-0 ml-0 list-none"
|
class="pt-2 reset-base list-none"
|
||||||
>
|
>
|
||||||
<SecondaryNavItem
|
<SecondaryNavItem
|
||||||
v-for="menuItem in accessibleMenuItems"
|
v-for="menuItem in accessibleMenuItems"
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export default {
|
|||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<ul v-if="hasSubMenu" class="mb-0 ml-0 list-none">
|
<ul v-if="hasSubMenu" class="reset-base list-none">
|
||||||
<SecondaryChildNavItem
|
<SecondaryChildNavItem
|
||||||
v-for="child in menuItem.children"
|
v-for="child in menuItem.children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('AccountSelector', () => {
|
|||||||
it('title and sub title exist', () => {
|
it('title and sub title exist', () => {
|
||||||
const headerComponent = accountSelector.findComponent(WootModalHeader);
|
const headerComponent = accountSelector.findComponent(WootModalHeader);
|
||||||
const title = headerComponent.find('[data-test-id="modal-header-title"]');
|
const title = headerComponent.find('[data-test-id="modal-header-title"]');
|
||||||
expect(title.text()).toBe('Switch Account');
|
expect(title.text()).toBe('Switch account');
|
||||||
const content = headerComponent.find(
|
const content = headerComponent.find(
|
||||||
'[data-test-id="modal-header-content"]'
|
'[data-test-id="modal-header-content"]'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -177,14 +177,16 @@
|
|||||||
},
|
},
|
||||||
"SIDEBAR_ITEMS": {
|
"SIDEBAR_ITEMS": {
|
||||||
"CHANGE_AVAILABILITY_STATUS": "Change",
|
"CHANGE_AVAILABILITY_STATUS": "Change",
|
||||||
"CHANGE_ACCOUNTS": "Switch Account",
|
"CHANGE_ACCOUNTS": "Switch account",
|
||||||
"CONTACT_SUPPORT": "Contact Support",
|
"SWITCH_WORKSPACE": "Switch workspace",
|
||||||
|
"CONTACT_SUPPORT": "Contact support",
|
||||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||||
"PROFILE_SETTINGS": "Profile Settings",
|
"PROFILE_SETTINGS": "Profile settings",
|
||||||
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
|
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||||
"APPEARANCE": "Change Appearance",
|
"APPEARANCE": "Change appearance",
|
||||||
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
|
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||||
"LOGOUT": "Logout"
|
"DOCS": "Read documentation",
|
||||||
|
"LOGOUT": "Log out"
|
||||||
},
|
},
|
||||||
"APP_GLOBAL": {
|
"APP_GLOBAL": {
|
||||||
"TRIAL_MESSAGE": "days trial remaining.",
|
"TRIAL_MESSAGE": "days trial remaining.",
|
||||||
@@ -279,6 +281,7 @@
|
|||||||
"REPORTS_INBOX": "Inbox",
|
"REPORTS_INBOX": "Inbox",
|
||||||
"REPORTS_TEAM": "Team",
|
"REPORTS_TEAM": "Team",
|
||||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||||
|
"SET_YOUR_AVAILABILITY": "Set your availability",
|
||||||
"SLA": "SLA",
|
"SLA": "SLA",
|
||||||
"CUSTOM_ROLES": "Custom Roles",
|
"CUSTOM_ROLES": "Custom Roles",
|
||||||
"BETA": "Beta",
|
"BETA": "Beta",
|
||||||
|
|||||||
@@ -151,8 +151,15 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAvailability: async ({ commit, dispatch }, params) => {
|
updateAvailability: async (
|
||||||
|
{ commit, dispatch, getters: _getters },
|
||||||
|
params
|
||||||
|
) => {
|
||||||
|
const previousStatus = _getters.getCurrentUserAvailability;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// optimisticly update current status
|
||||||
|
commit(types.SET_CURRENT_USER_AVAILABILITY, params.availability);
|
||||||
const response = await authAPI.updateAvailability(params);
|
const response = await authAPI.updateAvailability(params);
|
||||||
const userData = response.data;
|
const userData = response.data;
|
||||||
const { id } = userData;
|
const { id } = userData;
|
||||||
@@ -162,16 +169,23 @@ export const actions = {
|
|||||||
availabilityStatus: params.availability,
|
availabilityStatus: params.availability,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// revert back to previous status if update fails
|
||||||
|
commit(types.SET_CURRENT_USER_AVAILABILITY, previousStatus);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAutoOffline: async ({ commit }, { accountId, autoOffline }) => {
|
updateAutoOffline: async (
|
||||||
|
{ commit, getters: _getters },
|
||||||
|
{ accountId, autoOffline }
|
||||||
|
) => {
|
||||||
|
const previousAutoOffline = _getters.getCurrentUserAutoOffline;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
commit(types.SET_CURRENT_USER_AUTO_OFFLINE, autoOffline);
|
||||||
const response = await authAPI.updateAutoOffline(accountId, autoOffline);
|
const response = await authAPI.updateAutoOffline(accountId, autoOffline);
|
||||||
commit(types.SET_CURRENT_USER, response.data);
|
commit(types.SET_CURRENT_USER, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
commit(types.SET_CURRENT_USER_AUTO_OFFLINE, previousAutoOffline);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -212,6 +226,19 @@ export const mutations = {
|
|||||||
accounts,
|
accounts,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[types.SET_CURRENT_USER_AUTO_OFFLINE](_state, autoOffline) {
|
||||||
|
const accounts = _state.currentUser.accounts.map(account => {
|
||||||
|
if (account.id === _state.currentUser.account_id) {
|
||||||
|
return { ...account, autoOffline: autoOffline };
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
});
|
||||||
|
|
||||||
|
_state.currentUser = {
|
||||||
|
..._state.currentUser,
|
||||||
|
accounts,
|
||||||
|
};
|
||||||
|
},
|
||||||
[types.CLEAR_USER](_state) {
|
[types.CLEAR_USER](_state) {
|
||||||
_state.currentUser = initialState.currentUser;
|
_state.currentUser = initialState.currentUser;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { actions } from '../../auth';
|
import { actions } from '../../auth';
|
||||||
import * as types from '../../../mutation-types';
|
import types from '../../../mutation-types';
|
||||||
import * as APIHelpers from '../../../utils/api';
|
import * as APIHelpers from '../../../utils/api';
|
||||||
import '../../../../routes';
|
import '../../../../routes';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe('#actions', () => {
|
|||||||
await actions.validityCheck({ commit });
|
await actions.validityCheck({ commit });
|
||||||
expect(APIHelpers.setUser).toHaveBeenCalledTimes(1);
|
expect(APIHelpers.setUser).toHaveBeenCalledTimes(1);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CURRENT_USER, { id: 1, name: 'John' }],
|
[types.SET_CURRENT_USER, { id: 1, name: 'John' }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
@@ -45,7 +45,7 @@ describe('#actions', () => {
|
|||||||
});
|
});
|
||||||
await actions.updateProfile({ commit }, { name: 'Pranav' });
|
await actions.updateProfile({ commit }, { name: 'Pranav' });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CURRENT_USER, { id: 1, name: 'John' }],
|
[types.SET_CURRENT_USER, { id: 1, name: 'John' }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,12 +61,13 @@ describe('#actions', () => {
|
|||||||
headers: { expiry: 581842904 },
|
headers: { expiry: 581842904 },
|
||||||
});
|
});
|
||||||
await actions.updateAvailability(
|
await actions.updateAvailability(
|
||||||
{ commit, dispatch },
|
{ commit, dispatch, getters: { getCurrentUserAvailability: 'online' } },
|
||||||
{ availability: 'offline', account_id: 1 }
|
{ availability: 'offline', account_id: 1 }
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_CURRENT_USER_AVAILABILITY, 'offline'],
|
||||||
[
|
[
|
||||||
types.default.SET_CURRENT_USER,
|
types.SET_CURRENT_USER,
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'John',
|
name: 'John',
|
||||||
@@ -81,6 +82,18 @@ describe('#actions', () => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends correct actions if API is a failure', async () => {
|
||||||
|
axios.post.mockRejectedValue({ error: 'Authentication Failure' });
|
||||||
|
await actions.updateAvailability(
|
||||||
|
{ commit, dispatch, getters: { getCurrentUserAvailability: 'online' } },
|
||||||
|
{ availability: 'offline', account_id: 1 }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_CURRENT_USER_AVAILABILITY, 'offline'],
|
||||||
|
[types.SET_CURRENT_USER_AVAILABILITY, 'online'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#updateAutoOffline', () => {
|
describe('#updateAutoOffline', () => {
|
||||||
@@ -99,12 +112,13 @@ describe('#actions', () => {
|
|||||||
headers: { expiry: 581842904 },
|
headers: { expiry: 581842904 },
|
||||||
});
|
});
|
||||||
await actions.updateAutoOffline(
|
await actions.updateAutoOffline(
|
||||||
{ commit, dispatch },
|
{ commit, dispatch, getters: { getCurrentUserAutoOffline: true } },
|
||||||
{ autoOffline: false, accountId: 1 }
|
{ autoOffline: false, accountId: 1 }
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_CURRENT_USER_AUTO_OFFLINE, false],
|
||||||
[
|
[
|
||||||
types.default.SET_CURRENT_USER,
|
types.SET_CURRENT_USER,
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'John',
|
name: 'John',
|
||||||
@@ -113,6 +127,17 @@ describe('#actions', () => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('sends correct actions if API is failure', async () => {
|
||||||
|
axios.post.mockRejectedValue({ error: 'Authentication Failure' });
|
||||||
|
await actions.updateAutoOffline(
|
||||||
|
{ commit, dispatch, getters: { getCurrentUserAutoOffline: true } },
|
||||||
|
{ autoOffline: false, accountId: 1 }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_CURRENT_USER_AUTO_OFFLINE, false],
|
||||||
|
[types.SET_CURRENT_USER_AUTO_OFFLINE, true],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#updateUISettings', () => {
|
describe('#updateUISettings', () => {
|
||||||
@@ -132,11 +157,11 @@ describe('#actions', () => {
|
|||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
types.default.SET_CURRENT_USER_UI_SETTINGS,
|
types.SET_CURRENT_USER_UI_SETTINGS,
|
||||||
{ uiSettings: { is_contact_sidebar_open: false } },
|
{ uiSettings: { is_contact_sidebar_open: false } },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
types.default.SET_CURRENT_USER,
|
types.SET_CURRENT_USER,
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'John',
|
name: 'John',
|
||||||
@@ -160,8 +185,8 @@ describe('#actions', () => {
|
|||||||
Cookies.get.mockImplementation(() => false);
|
Cookies.get.mockImplementation(() => false);
|
||||||
actions.setUser({ commit, dispatch });
|
actions.setUser({ commit, dispatch });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.CLEAR_USER],
|
[types.CLEAR_USER],
|
||||||
[types.default.SET_CURRENT_USER_UI_FLAGS, { isFetching: false }],
|
[types.SET_CURRENT_USER_UI_FLAGS, { isFetching: false }],
|
||||||
]);
|
]);
|
||||||
expect(dispatch).toHaveBeenCalledTimes(0);
|
expect(dispatch).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
@@ -177,7 +202,7 @@ describe('#actions', () => {
|
|||||||
{ 1: 'online' }
|
{ 1: 'online' }
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CURRENT_USER_AVAILABILITY, 'online'],
|
[types.SET_CURRENT_USER_AVAILABILITY, 'online'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
CLEAR_USER: 'LOGOUT',
|
CLEAR_USER: 'LOGOUT',
|
||||||
SET_CURRENT_USER: 'SET_CURRENT_USER',
|
SET_CURRENT_USER: 'SET_CURRENT_USER',
|
||||||
SET_CURRENT_USER_AVAILABILITY: 'SET_CURRENT_USER_AVAILABILITY',
|
SET_CURRENT_USER_AVAILABILITY: 'SET_CURRENT_USER_AVAILABILITY',
|
||||||
|
SET_CURRENT_USER_AUTO_OFFLINE: 'SET_CURRENT_USER_AUTO_OFFLINE',
|
||||||
SET_CURRENT_USER_UI_SETTINGS: 'SET_CURRENT_USER_UI_SETTINGS',
|
SET_CURRENT_USER_UI_SETTINGS: 'SET_CURRENT_USER_UI_SETTINGS',
|
||||||
SET_CURRENT_USER_UI_FLAGS: 'SET_CURRENT_USER_UI_FLAGS',
|
SET_CURRENT_USER_UI_FLAGS: 'SET_CURRENT_USER_UI_FLAGS',
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user