mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Updates sidebar to accomodate sub menu (#3416)
* Enhancement: Updates sidebar to a new design (#2733) * feat: Changes primary navbar to new design (#2598) * feat: updates design for secondary navbar (#2612) * Changes primary nvbar to new design * Updates design for contexual sidebar * Fixes issues with JSON * Remove duplication of notificatons in Navigation * Fixes broken tests * Fixes broken tests * Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue * Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue * Update app/javascript/dashboard/components/layout/SidebarItem.vue Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> * Update app/javascript/dashboard/components/layout/SidebarItem.vue Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> * Update app/javascript/dashboard/modules/sidebar/components/Secondary.vue Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> * Chore: Update design changes to features * Fixes menu transitions and refactors code * Refactors sidebar routeing logic * lint error fixes * Fixes dropdown menu styles * Fixes secondary new item links * Fixes lint scss issues * fixes linter issues * Fixes broken test cases * Update AvailabilityStatus.spec.js * Review feedbacks * Fixes add modal for label * Add tooltip for primary menu item * Tooltip for notifications * Adds tooltip for primary menu items * Review fixes * Review fixes * Fix merge issues * fixes logo size for login pages * fixes Merge breaks with styles Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		 Nithin David Thomas
					Nithin David Thomas
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							c792cfc0be
						
					
				
				
					commit
					b01d032d0d
				
			| @@ -1,4 +1,3 @@ | ||||
|  | ||||
| /* Enter and leave animations can use different */ | ||||
| /* durations and timing functions.              */ | ||||
| .slide-fade-enter-active { | ||||
| @@ -9,7 +8,8 @@ | ||||
|   transition: all .3s $ease-out-cubic; | ||||
| } | ||||
|  | ||||
| .slide-fade-enter, .slide-fade-leave-to { | ||||
| .slide-fade-enter, | ||||
| .slide-fade-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateX(10px); | ||||
| } | ||||
| @@ -22,22 +22,33 @@ | ||||
|   transform: translateX($space-medium); | ||||
| } | ||||
|  | ||||
| .conversations-list-enter-active, .conversations-list-leave-active { | ||||
| .conversations-list-enter-active, | ||||
| .conversations-list-leave-active { | ||||
|   transition: all .25s $ease-out-cubic; | ||||
| } | ||||
|  | ||||
| .conversations-list-enter, .conversations-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { | ||||
| .conversations-list-enter, | ||||
| .conversations-list-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateX($space-medium); | ||||
| } | ||||
|  | ||||
| .menu-list-enter-active, .menu-list-leave-active { | ||||
|   transition: all .2s $ease-out-cubic; | ||||
| .menu-list-enter-active, | ||||
| .menu-list-leave-active { | ||||
|   transition: opacity .3s $ease-out-cubic, | ||||
|     transform .2s $ease-out-cubic; | ||||
| } | ||||
|  | ||||
| .menu-list-enter, .menu-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { | ||||
|  | ||||
| .menu-list-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateX($space-medium); | ||||
|   position: absolute; | ||||
|   transform: translateX($space-small); | ||||
| } | ||||
|  | ||||
| .menu-list-enter { | ||||
|   opacity: 0; | ||||
|   transform: translateX(-$space-small); | ||||
| } | ||||
|  | ||||
| .slide-up-enter-active { | ||||
| @@ -48,8 +59,8 @@ | ||||
|   transition: all .3s $ease-out-cubic; | ||||
| } | ||||
|  | ||||
| .slide-up-enter, .slide-up-leave-to | ||||
| /* .slide-fade-leave-active for <2.1.8 */ { | ||||
| .slide-up-enter, | ||||
| .slide-up-leave-to { | ||||
|   transform: translateY(-$space-medium); | ||||
|   opacity: 0; | ||||
| } | ||||
| @@ -60,10 +71,10 @@ | ||||
|   transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic; | ||||
| } | ||||
|  | ||||
| .menu-slide-enter, .menu-slide-leave-to | ||||
| /* .slide-fade-leave-active for <2.1.8 */ { | ||||
|   transform: translateY($space-small); | ||||
| .menu-slide-enter, | ||||
| .menu-slide-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateY($space-small); | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -75,10 +86,10 @@ | ||||
|   transition: all .1s $ease-out-sine; | ||||
| } | ||||
|  | ||||
| .toast-fade-enter, .toast-fade-leave-to | ||||
| /* .toast-fade-leave-active for <2.1.8 */ { | ||||
|   transform: translateY(-$space-small); | ||||
| .toast-fade-enter, | ||||
| .toast-fade-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateY(-$space-small); | ||||
| } | ||||
|  | ||||
| .modal-fade-enter-active { | ||||
| @@ -89,8 +100,8 @@ | ||||
|   transition: all .1s $ease-out-sine; | ||||
| } | ||||
|  | ||||
| .modal-fade-enter, .modal-fade-leave-to | ||||
| /* .slide-fade-leave-active for <2.1.8 */ { | ||||
| .modal-fade-enter, | ||||
| .modal-fade-leave-to { | ||||
|   opacity: 0; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -56,6 +56,10 @@ code { | ||||
| } | ||||
|  | ||||
|  | ||||
| .badge { | ||||
|   border-radius: var(--border-radius-normal); | ||||
| } | ||||
|  | ||||
| .padding-right-small { | ||||
|   padding-right: var(--space-one); | ||||
| } | ||||
|   | ||||
| @@ -219,9 +219,9 @@ $badge-background: $primary-color; | ||||
| $badge-color: $white; | ||||
| $badge-color-alt: $black; | ||||
| $badge-palette: $foundation-palette; | ||||
| $badge-padding: 0.3em; | ||||
| $badge-padding: var(--space-smaller); | ||||
| $badge-minwidth: 2.1em; | ||||
| $badge-font-size: 0.6rem; | ||||
| $badge-font-size: var(--font-size-nano); | ||||
|  | ||||
| // 10. Breadcrumbs | ||||
| // --------------- | ||||
| @@ -400,7 +400,7 @@ $mediaobject-image-width-stacked: 100%; | ||||
|  | ||||
| $menu-margin: 0; | ||||
| $menu-margin-nested: $space-medium; | ||||
| $menu-item-padding: $space-one; | ||||
| $menu-item-padding: $space-slab; | ||||
| $menu-item-color-active: $white; | ||||
| $menu-item-background-active: $color-background; | ||||
| $menu-icon-spacing: 0.25rem; | ||||
|   | ||||
| @@ -44,11 +44,14 @@ $woot-logo-padding: $space-large $space-two; | ||||
| $color-woot: #1f93ff; | ||||
| $color-gray: #6e6f73; | ||||
| $color-light-gray: #999a9b; | ||||
| $color-border: #e0e6ed; | ||||
| $color-border-light: #f0f4f5; | ||||
| $color-border-dark: #cad0d4; | ||||
| $color-background: #f4f6fb; | ||||
| $color-background-light: #f9fafc; | ||||
|  | ||||
| $color-border: var(--s-75); | ||||
| $color-border-light: var(--s-50); | ||||
| $color-border-dark: var(--s-100); | ||||
|  | ||||
| $color-background: var(--s-50); | ||||
| $color-background-light: var(--s-25); | ||||
|  | ||||
| $color-white: #fff; | ||||
| $color-body: #3c4858; | ||||
| $color-heading: #1f2d3d; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|   @include elegant-card; | ||||
|   @include border-light; | ||||
|   box-sizing: content-box; | ||||
|   padding: var(--space-small); | ||||
|   width: fit-content; | ||||
|   z-index: var(--z-index-very-high); | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|   @include background-white; | ||||
|   @include flex; | ||||
|   @include flex-align($x: justify, $y: middle); | ||||
|   @include border-normal-bottom; | ||||
|   border-bottom: 1px solid var(--s-50); | ||||
|   height: $header-height; | ||||
|   min-height: $header-height; | ||||
|  | ||||
|   | ||||
| @@ -6,12 +6,6 @@ | ||||
| } | ||||
|  | ||||
| .sidebar { | ||||
|   @include border-normal-right; | ||||
|   @include background-white; | ||||
|   @include full-height; | ||||
|   @include margin(0); | ||||
|   @include space-between-column; | ||||
|   width: $nav-bar-width; | ||||
|   z-index: 1024 - 1; | ||||
|  | ||||
|   //logo | ||||
| @@ -22,28 +16,6 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .main-nav { | ||||
|     a { | ||||
|       border-radius: $space-smaller; | ||||
|       color: $color-gray; | ||||
|       font-size: $font-size-default; | ||||
|       font-weight: $font-weight-medium; | ||||
|  | ||||
|       .wrap, | ||||
|       .child-icon { | ||||
|         color: $color-gray; | ||||
|  | ||||
|         &:hover { | ||||
|           color: $color-woot; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .active a .wrap { | ||||
|       color: $color-woot; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .nested { | ||||
|     a { | ||||
|       font-size: $font-size-small; | ||||
| @@ -64,7 +36,7 @@ | ||||
| .bottom-nav { | ||||
|   @include flex; | ||||
|   @include space-between-column; | ||||
|   @include padding($space-one); | ||||
|   @include padding($space-one $space-normal $space-one $space-one); | ||||
|   @include border-normal-top; | ||||
|   flex-direction: column; | ||||
|   position: relative; | ||||
| @@ -85,32 +57,14 @@ | ||||
|   } | ||||
| } | ||||
|  | ||||
| .main-nav { | ||||
|   @include flex-weight(1); | ||||
|   @include scroll-on-hover; | ||||
|   padding: 0 $space-medium - $space-one; | ||||
| .hamburger--menu { | ||||
|   cursor: pointer; | ||||
|   display: none; | ||||
|   margin-right: $space-normal; | ||||
|  | ||||
|   a { | ||||
|     &::before { | ||||
|       margin-right: $space-slab; | ||||
|     } | ||||
|   @media screen and (max-width: 1200px) { | ||||
|     display: block; | ||||
|   } | ||||
|  | ||||
|   .menu-title { | ||||
|     color: $color-gray; | ||||
|     font-size: $font-size-medium; | ||||
|     margin-top: $space-medium; | ||||
|  | ||||
|     >span { | ||||
|       margin-left: $space-one; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .menu-title+ul>li>a { | ||||
|   @include padding($space-micro null); | ||||
|   color: $medium-gray; | ||||
|   line-height: $global-lineheight; | ||||
| } | ||||
|  | ||||
| .header--icon { | ||||
|   | ||||
| @@ -1,66 +1,41 @@ | ||||
| <template> | ||||
|   <div class="status"> | ||||
|     <div class="status-view"> | ||||
|       <availability-status-badge :status="currentUserAvailability" /> | ||||
|       <div class="status-view--title"> | ||||
|         {{ availabilityDisplayLabel }} | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="status-change"> | ||||
|       <transition name="menu-slide"> | ||||
|         <div | ||||
|           v-if="isStatusMenuOpened" | ||||
|           v-on-clickaway="closeStatusMenu" | ||||
|           class="dropdown-pane dropdowm--top" | ||||
|         > | ||||
|           <woot-dropdown-menu> | ||||
|             <woot-dropdown-item | ||||
|               v-for="status in availabilityStatuses" | ||||
|               :key="status.value" | ||||
|               class="status-items" | ||||
|             > | ||||
|               <woot-button | ||||
|                 variant="clear" | ||||
|                 size="small" | ||||
|                 color-scheme="secondary" | ||||
|                 class-names="status-change--dropdown-button" | ||||
|                 :is-disabled="status.disabled" | ||||
|                 @click=" | ||||
|                   changeAvailabilityStatus(status.value, currentAccountId) | ||||
|                 " | ||||
|               > | ||||
|                 <availability-status-badge :status="status.value" /> | ||||
|                 {{ status.label }} | ||||
|               </woot-button> | ||||
|             </woot-dropdown-item> | ||||
|           </woot-dropdown-menu> | ||||
|         </div> | ||||
|       </transition> | ||||
|  | ||||
|   <woot-dropdown-menu> | ||||
|     <woot-dropdown-header :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" /> | ||||
|     <woot-dropdown-item | ||||
|       v-for="status in availabilityStatuses" | ||||
|       :key="status.value" | ||||
|       class="status-items" | ||||
|     > | ||||
|       <woot-button | ||||
|         variant="clear" | ||||
|         color-scheme="secondary" | ||||
|         class-names="status-change--change-button link" | ||||
|         @click="openStatusMenu" | ||||
|         size="small" | ||||
|         :color-scheme="status.disabled ? '' : 'secondary'" | ||||
|         :variant="status.disabled ? 'smooth' : 'clear'" | ||||
|         class-names="status-change--dropdown-button" | ||||
|         @click="changeAvailabilityStatus(status.value)" | ||||
|       > | ||||
|         {{ $t('SIDEBAR_ITEMS.CHANGE_AVAILABILITY_STATUS') }} | ||||
|         <availability-status-badge :status="status.value" /> | ||||
|         {{ status.label }} | ||||
|       </woot-button> | ||||
|     </div> | ||||
|   </div> | ||||
|     </woot-dropdown-item> | ||||
|     <woot-dropdown-divider /> | ||||
|   </woot-dropdown-menu> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import { mixin as clickaway } from 'vue-clickaway'; | ||||
| import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; | ||||
| import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; | ||||
| import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; | ||||
| import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider'; | ||||
| import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge'; | ||||
|  | ||||
| const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline']; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     WootDropdownHeader, | ||||
|     WootDropdownDivider, | ||||
|     WootDropdownMenu, | ||||
|     WootDropdownItem, | ||||
|     AvailabilityStatusBadge, | ||||
| @@ -100,8 +75,7 @@ export default { | ||||
|           label: statusLabel, | ||||
|           value: AVAILABILITY_STATUS_KEYS[index], | ||||
|           disabled: | ||||
|             this.currentUserAvailability === | ||||
|             AVAILABILITY_STATUS_KEYS[index], | ||||
|             this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index], | ||||
|         }) | ||||
|       ); | ||||
|     }, | ||||
| @@ -114,7 +88,8 @@ export default { | ||||
|     closeStatusMenu() { | ||||
|       this.isStatusMenuOpened = false; | ||||
|     }, | ||||
|     changeAvailabilityStatus(availability, accountId) { | ||||
|     changeAvailabilityStatus(availability) { | ||||
|       const accountId = this.currentAccountId; | ||||
|       if (this.isUpdating) { | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -1,59 +1,23 @@ | ||||
| <template> | ||||
|   <aside class="sidebar animated shrink columns"> | ||||
|     <div class="logo"> | ||||
|       <router-link :to="dashboardPath" replace> | ||||
|         <img :src="globalConfig.logo" :alt="globalConfig.installationName" /> | ||||
|       </router-link> | ||||
|     </div> | ||||
|   <aside class="woot-sidebar" :class="{ 'only-primary': !showSecondaryMenu }"> | ||||
|     <primary-sidebar | ||||
|       :logo-source="globalConfig.logo" | ||||
|       :installation-name="globalConfig.installationName" | ||||
|       :account-id="accountId" | ||||
|       :menu-items="primaryMenuItems" | ||||
|       @toggle-accounts="toggleAccountModal" | ||||
|       @key-shortcut-modal="toggleKeyShortcutModal" | ||||
|     /> | ||||
|  | ||||
|     <div class="main-nav"> | ||||
|       <transition-group name="menu-list" tag="ul" class="menu vertical"> | ||||
|         <sidebar-item | ||||
|           v-for="item in accessibleMenuItems" | ||||
|           :key="item.toState" | ||||
|           :menu-item="item" | ||||
|         /> | ||||
|         <sidebar-item | ||||
|           v-if="shouldShowTeams" | ||||
|           :key="teamSection.toState" | ||||
|           :menu-item="teamSection" | ||||
|         /> | ||||
|         <sidebar-item | ||||
|           v-if="shouldShowSidebarItem" | ||||
|           :key="inboxSection.toState" | ||||
|           :menu-item="inboxSection" | ||||
|         /> | ||||
|         <sidebar-item | ||||
|           v-if="shouldShowSidebarItem" | ||||
|           :key="labelSection.toState" | ||||
|           :menu-item="labelSection" | ||||
|           @add-label="showAddLabelPopup" | ||||
|         /> | ||||
|         <sidebar-item | ||||
|           v-if="showShowContactSideMenu" | ||||
|           :key="contactLabelSection.key" | ||||
|           :menu-item="contactLabelSection" | ||||
|           @add-label="showAddLabelPopup" | ||||
|         /> | ||||
|       </transition-group> | ||||
|     </div> | ||||
|  | ||||
|     <div class="bottom-nav"> | ||||
|       <availability-status /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="bottom-nav app-context-menu" @click="toggleOptions"> | ||||
|       <agent-details @show-options="toggleOptions" /> | ||||
|       <notification-bell /> | ||||
|       <fluent-icon class="current-user--options" icon="more-vertical" /> | ||||
|       <options-menu | ||||
|         :show="showOptionsMenu" | ||||
|         @toggle-accounts="toggleAccountModal" | ||||
|         @show-support-chat-window="toggleSupportChatWindow" | ||||
|         @key-shortcut-modal="toggleKeyShortcutModal" | ||||
|         @close="toggleOptions" | ||||
|       /> | ||||
|     </div> | ||||
|     <secondary-sidebar | ||||
|       v-if="showSecondaryMenu" | ||||
|       :account-id="accountId" | ||||
|       :inboxes="inboxes" | ||||
|       :account-labels="accountLabels" | ||||
|       :teams="teams" | ||||
|       :menu-items="primaryMenuItems" | ||||
|       @add-label="showAddLabelPopup" | ||||
|     /> | ||||
|  | ||||
|     <woot-key-shortcut-modal | ||||
|       v-if="showShortcutModal" | ||||
| @@ -82,17 +46,14 @@ | ||||
| import { mapGetters } from 'vuex'; | ||||
|  | ||||
| import adminMixin from '../../mixins/isAdmin'; | ||||
| import SidebarItem from './SidebarItem'; | ||||
| import AvailabilityStatus from './AvailabilityStatus'; | ||||
| import { frontendURL } from '../../helper/URLHelper'; | ||||
| import { getSidebarItems } from '../../i18n/default-sidebar'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
| import NotificationBell from './sidebarComponents/NotificationBell'; | ||||
| import AgentDetails from './sidebarComponents/AgentDetails.vue'; | ||||
| import OptionsMenu from './sidebarComponents/OptionsMenu.vue'; | ||||
|  | ||||
| import AccountSelector from './sidebarComponents/AccountSelector.vue'; | ||||
| import AddAccountModal from './sidebarComponents/AddAccountModal.vue'; | ||||
| import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel'; | ||||
| import PrimarySidebar from 'dashboard/modules/sidebar/components/Primary'; | ||||
| import SecondarySidebar from 'dashboard/modules/sidebar/components/Secondary'; | ||||
| import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal'; | ||||
| import { | ||||
|   hasPressedAltAndCKey, | ||||
| @@ -110,11 +71,8 @@ export default { | ||||
|     AccountSelector, | ||||
|     AddAccountModal, | ||||
|     AddLabelModal, | ||||
|     AgentDetails, | ||||
|     AvailabilityStatus, | ||||
|     NotificationBell, | ||||
|     OptionsMenu, | ||||
|     SidebarItem, | ||||
|     PrimarySidebar, | ||||
|     SecondarySidebar, | ||||
|     WootKeyShortcutModal, | ||||
|   }, | ||||
|   mixins: [adminMixin, alertMixin, eventListenerMixins], | ||||
| @@ -139,125 +97,34 @@ export default { | ||||
|       teams: 'teams/getMyTeams', | ||||
|     }), | ||||
|  | ||||
|     sidemenuItems() { | ||||
|     sideMenuItems() { | ||||
|       return getSidebarItems(this.accountId); | ||||
|     }, | ||||
|     accessibleMenuItems() { | ||||
|       // get all keys in menuGroup | ||||
|       const groupKey = Object.keys(this.sidemenuItems); | ||||
|     primaryMenuItems() { | ||||
|       const menuItems = Object.values( | ||||
|         getSidebarItems(this.accountId).common.menuItems | ||||
|       ); | ||||
|  | ||||
|       let menuItems = []; | ||||
|       // Iterate over menuGroup to find the correct group | ||||
|       for (let i = 0; i < groupKey.length; i += 1) { | ||||
|         const groupItem = this.sidemenuItems[groupKey[i]]; | ||||
|         // Check if current route is included | ||||
|         const isRouteIncluded = groupItem.routes.includes(this.currentRoute); | ||||
|         if (isRouteIncluded) { | ||||
|           menuItems = Object.values(groupItem.menuItems); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return this.filterMenuItemsByRole(menuItems); | ||||
|       return menuItems; | ||||
|     }, | ||||
|     currentRoute() { | ||||
|       return this.$store.state.route.name; | ||||
|     }, | ||||
|     shouldShowSidebarItem() { | ||||
|       return this.sidemenuItems.common.routes.includes(this.currentRoute); | ||||
|     shouldShowNotificationsSideMenu() { | ||||
|       return this.sideMenuItems.notifications.routes.includes( | ||||
|         this.currentRoute | ||||
|       ); | ||||
|     }, | ||||
|     showShowContactSideMenu() { | ||||
|       return this.sidemenuItems.contacts.routes.includes(this.currentRoute); | ||||
|     shouldShowProfileSideMenu() { | ||||
|       return ( | ||||
|         this.currentRoute === 'profile_settings_index' || | ||||
|         this.currentRoute === 'profile_settings' | ||||
|       ); | ||||
|     }, | ||||
|     shouldShowTeams() { | ||||
|       return this.shouldShowSidebarItem && this.teams.length; | ||||
|     }, | ||||
|     inboxSection() { | ||||
|       return { | ||||
|         icon: 'folder', | ||||
|         label: 'INBOXES', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         key: 'inbox', | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`), | ||||
|         toStateName: 'settings_inbox_list', | ||||
|         newLinkRouteName: 'settings_inbox_new', | ||||
|         children: this.inboxes.map(inbox => ({ | ||||
|           id: inbox.id, | ||||
|           label: inbox.name, | ||||
|           toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`), | ||||
|           type: inbox.channel_type, | ||||
|           phoneNumber: inbox.phone_number, | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     labelSection() { | ||||
|       return { | ||||
|         icon: 'number-symbol', | ||||
|         label: 'LABELS', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         key: 'label', | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/labels`), | ||||
|         toStateName: 'labels_list', | ||||
|         showModalForNewItem: true, | ||||
|         modalName: 'AddLabel', | ||||
|         children: this.accountLabels.map(label => ({ | ||||
|           id: label.id, | ||||
|           label: label.title, | ||||
|           color: label.color, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL( | ||||
|             `accounts/${this.accountId}/label/${label.title}` | ||||
|           ), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     contactLabelSection() { | ||||
|       return { | ||||
|         icon: 'number-symbol', | ||||
|         label: 'TAGGED_WITH', | ||||
|         hasSubMenu: true, | ||||
|         key: 'label', | ||||
|         newLink: false, | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/labels`), | ||||
|         toStateName: 'labels_list', | ||||
|         showModalForNewItem: true, | ||||
|         modalName: 'AddLabel', | ||||
|         children: this.accountLabels.map(label => ({ | ||||
|           id: label.id, | ||||
|           label: label.title, | ||||
|           color: label.color, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL( | ||||
|             `accounts/${this.accountId}/labels/${label.title}/contacts` | ||||
|           ), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     teamSection() { | ||||
|       return { | ||||
|         icon: 'people-team', | ||||
|         label: 'TEAMS', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         key: 'team', | ||||
|         cssClass: 'menu-title align-justify teams-sidebar-menu', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/teams`), | ||||
|         toStateName: 'teams_list', | ||||
|         newLinkRouteName: 'settings_teams_new', | ||||
|         children: this.teams.map(team => ({ | ||||
|           id: team.id, | ||||
|           label: team.name, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     dashboardPath() { | ||||
|       return frontendURL(`accounts/${this.accountId}/dashboard`); | ||||
|     showSecondaryMenu() { | ||||
|       if (this.shouldShowNotificationsSideMenu) return false; | ||||
|       if (this.shouldShowProfileSideMenu) return false; | ||||
|       return true; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
| @@ -318,9 +185,7 @@ export default { | ||||
|           ) > -1 | ||||
|       ); | ||||
|     }, | ||||
|     toggleOptions() { | ||||
|       this.showOptionsMenu = !this.showOptionsMenu; | ||||
|     }, | ||||
|  | ||||
|     toggleAccountModal() { | ||||
|       this.showAccountModal = !this.showAccountModal; | ||||
|     }, | ||||
| @@ -341,6 +206,27 @@ export default { | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .woot-sidebar { | ||||
|   background: white; | ||||
|   display: flex; | ||||
|  | ||||
|   &.only-primary { | ||||
|     width: auto; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .secondary-menu { | ||||
|   background: var(--white); | ||||
|   border-right: 1px solid var(--s-50); | ||||
|   height: 100vh; | ||||
|   width: 19rem; | ||||
|   flex-shrink: 0; | ||||
|   overflow: auto; | ||||
|   padding: var(--space-small); | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss"> | ||||
| @import '~dashboard/assets/scss/variables'; | ||||
|  | ||||
| @@ -405,7 +291,7 @@ export default { | ||||
|   margin-top: auto; | ||||
| } | ||||
|  | ||||
| .teams-sidebar-menu + .nested.vertical.menu { | ||||
|   padding-left: calc(var(--space-medium) - var(--space-one)); | ||||
| .secondary-menu .nested.vertical.menu { | ||||
|   margin-left: var(--space-small); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,10 +1,5 @@ | ||||
| <template> | ||||
|   <router-link | ||||
|     :to="menuItem.toState" | ||||
|     tag="li" | ||||
|     active-class="active" | ||||
|     :class="computedClass" | ||||
|   > | ||||
|   <li :class="computedClass" class="sidebar-item"> | ||||
|     <a | ||||
|       class="sub-menu-title" | ||||
|       :class="getMenuItemClass" | ||||
| @@ -12,76 +7,61 @@ | ||||
|       aria-haspopup="true" | ||||
|       :title="menuItem.toolTip" | ||||
|     > | ||||
|       <div class="wrap"> | ||||
|         <fluent-icon | ||||
|           size="18" | ||||
|           :icon="menuItem.icon" | ||||
|           class="margin-right-small" | ||||
|         /> | ||||
|         {{ $t(`SIDEBAR.${menuItem.label}`) }} | ||||
|       </div> | ||||
|       <button | ||||
|         v-if="showItem(menuItem)" | ||||
|         class="child-icon" | ||||
|         @click.prevent="newLinkClick(menuItem)" | ||||
|       > | ||||
|         <fluent-icon icon="add-circle" size="16" /> | ||||
|       </button> | ||||
|       {{ $t(`SIDEBAR.${menuItem.label}`) }} | ||||
|     </a> | ||||
|     <ul v-if="menuItem.hasSubMenu" class="nested vertical menu"> | ||||
|       <router-link | ||||
|       <secondary-nav-item | ||||
|         v-for="child in menuItem.children" | ||||
|         :key="child.id" | ||||
|         active-class="active flex-container" | ||||
|         tag="li" | ||||
|         :to="child.toState" | ||||
|         :label="child.label" | ||||
|         :label-color="child.color" | ||||
|         :should-truncate="child.truncateLabel" | ||||
|         :icon="computedInboxClass(child)" | ||||
|       /> | ||||
|       <router-link | ||||
|         v-if="menuItem.newLink" | ||||
|         v-slot="{ href, isActive, navigate }" | ||||
|         :to="menuItem.toState" | ||||
|         custom | ||||
|       > | ||||
|         <a href="#" :class="computedChildClass(child)"> | ||||
|           <div class="wrap"> | ||||
|             <fluent-icon | ||||
|               v-if="menuItem.key === 'inbox'" | ||||
|               class="inbox-icon" | ||||
|               size="14" | ||||
|               :icon="computedInboxClass(child)" | ||||
|             /> | ||||
|             <span | ||||
|               v-if="child.color" | ||||
|               class="label-color--display" | ||||
|               :style="{ backgroundColor: child.color }" | ||||
|             /> | ||||
|             <div | ||||
|               :title="computedChildTitle(child)" | ||||
|               :class="computedChildClass(child)" | ||||
|             > | ||||
|               {{ child.label }} | ||||
|             </div> | ||||
|           </div> | ||||
|         </a> | ||||
|         <li> | ||||
|           <a | ||||
|             :href="href" | ||||
|             class="button small clear menu-item--new secondary" | ||||
|             :class="{ 'is-active': isActive }" | ||||
|             @click="e => newLinkClick(e, navigate)" | ||||
|           > | ||||
|             <fluent-icon icon="add" /> | ||||
|             <span class="button__content"> | ||||
|               {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} | ||||
|             </span> | ||||
|           </a> | ||||
|         </li> | ||||
|       </router-link> | ||||
|     </ul> | ||||
|   </router-link> | ||||
|   </li> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
|  | ||||
| import router from '../../routes'; | ||||
| import adminMixin from '../../mixins/isAdmin'; | ||||
| import { getInboxClassByType } from 'dashboard/helper/inbox'; | ||||
|  | ||||
| import SecondaryNavItem from 'dashboard/modules/sidebar/components/SecondaryNavItem'; | ||||
|  | ||||
| export default { | ||||
|   components: { SecondaryNavItem }, | ||||
|   mixins: [adminMixin], | ||||
|   props: { | ||||
|     menuItem: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return {}; | ||||
|       }, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       activeInbox: 'getSelectedInbox', | ||||
|     }), | ||||
|     ...mapGetters({ activeInbox: 'getSelectedInbox' }), | ||||
|     getMenuItemClass() { | ||||
|       return this.menuItem.cssClass | ||||
|         ? `side-menu ${this.menuItem.cssClass}` | ||||
| @@ -104,6 +84,7 @@ export default { | ||||
|   methods: { | ||||
|     computedInboxClass(child) { | ||||
|       const { type, phoneNumber } = child; | ||||
|       if (!type) return ''; | ||||
|       const classByType = getInboxClassByType(type, phoneNumber); | ||||
|       return classByType; | ||||
|     }, | ||||
| @@ -115,11 +96,12 @@ export default { | ||||
|       if (!child.truncateLabel) return false; | ||||
|       return child.label; | ||||
|     }, | ||||
|     newLinkClick(item) { | ||||
|       if (item.newLinkRouteName) { | ||||
|         router.push({ name: item.newLinkRouteName, params: { page: 'new' } }); | ||||
|       } else if (item.showModalForNewItem) { | ||||
|         if (item.modalName === 'AddLabel') { | ||||
|     newLinkClick(e, navigate) { | ||||
|       if (this.menuItem.newLinkRouteName) { | ||||
|         navigate(e); | ||||
|       } else if (this.menuItem.showModalForNewItem) { | ||||
|         if (this.menuItem.modalName === 'AddLabel') { | ||||
|           e.preventDefault(); | ||||
|           this.$emit('add-label'); | ||||
|         } | ||||
|       } | ||||
| @@ -131,11 +113,22 @@ export default { | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| @import '~dashboard/assets/scss/variables'; | ||||
|  | ||||
| .sidebar-item { | ||||
|   margin: var(--space-small) 0; | ||||
| } | ||||
| .sub-menu-title { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   padding: 0 var(--space-small); | ||||
|   margin-bottom: var(--space-smaller); | ||||
|   color: var(--s-600); | ||||
|   font-weight: var(--font-weight-bold); | ||||
|   line-height: var(--space-two); | ||||
|   text-transform: uppercase; | ||||
| } | ||||
|  | ||||
| .sub-menu-link { | ||||
|   color: var(--s-600); | ||||
| } | ||||
|  | ||||
| .wrap { | ||||
| @@ -144,10 +137,27 @@ export default { | ||||
| } | ||||
|  | ||||
| .label-color--display { | ||||
|   border-radius: $space-smaller; | ||||
|   height: $space-normal; | ||||
|   margin-right: $space-small; | ||||
|   min-width: $space-normal; | ||||
|   width: $space-normal; | ||||
|   border-radius: var(--space-smaller); | ||||
|   height: var(--space-normal); | ||||
|   margin-right: var(--space-small); | ||||
|   min-width: var(--space-normal); | ||||
|   width: var(--space-normal); | ||||
| } | ||||
|  | ||||
| .inbox-icon { | ||||
|   position: relative; | ||||
|   top: -1px; | ||||
| } | ||||
|  | ||||
| .sidebar-item .button.menu-item--new { | ||||
|   display: inline-flex; | ||||
|   height: var(--space-medium); | ||||
|   margin: var(--space-smaller) 0; | ||||
|   padding: var(--space-smaller); | ||||
|   color: var(--s-500); | ||||
|  | ||||
|   &:hover { | ||||
|     color: var(--w-500); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| <template> | ||||
|   <div class="current-user--row"> | ||||
|   <woot-button | ||||
|     v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)" | ||||
|     variant="link" | ||||
|     class="current-user" | ||||
|     @click="handleClick" | ||||
|   > | ||||
|     <thumbnail | ||||
|       :src="currentUser.avatar_url" | ||||
|       :username="currentUserAvailableName" | ||||
|       :username="currentUser.name" | ||||
|       :status="statusOfAgent" | ||||
|       should-show-status-always | ||||
|       size="32px" | ||||
|     /> | ||||
|     <div class="current-user--data"> | ||||
|       <h3 class="current-user--name text-truncate"> | ||||
|         {{ currentUserAvailableName }} | ||||
|       </h3> | ||||
|       <h5 v-if="currentRole" class="current-user--role"> | ||||
|         {{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }} | ||||
|       </h5> | ||||
|     </div> | ||||
|   </div> | ||||
|   </woot-button> | ||||
| </template> | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| @@ -25,39 +25,25 @@ export default { | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       currentUser: 'getCurrentUser', | ||||
|       currentRole: 'getCurrentRole', | ||||
|       currentUserAvailability: 'getCurrentUserAvailability', | ||||
|     }), | ||||
|     currentUserAvailableName() { | ||||
|       return this.currentUser.name; | ||||
|     statusOfAgent() { | ||||
|       return this.currentUserAvailability || 'offline'; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     handleClick() { | ||||
|       this.$emit('toggle-menu'); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .current-user--row { | ||||
| .current-user { | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
| } | ||||
|  | ||||
| .current-user--data { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|  | ||||
|   .current-user--name { | ||||
|     font-size: var(--font-size-small); | ||||
|     font-weight: var(--font-weight-medium); | ||||
|     margin-bottom: var(--space-micro); | ||||
|     margin-left: var(--space-one); | ||||
|     max-width: 12rem; | ||||
|   } | ||||
|  | ||||
|   .current-user--role { | ||||
|     color: var(--color-gray); | ||||
|     font-size: var(--font-size-mini); | ||||
|     margin-bottom: var(--zero); | ||||
|     margin-left: var(--space-one); | ||||
|     text-transform: capitalize; | ||||
|   } | ||||
|   border-radius: 50%; | ||||
|   border: 2px solid var(--white); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,13 +1,19 @@ | ||||
| <template> | ||||
|   <span class="notifications" @click.stop="showNotification"> | ||||
|     <fluent-icon icon="alert" /> | ||||
|     <span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span> | ||||
|   </span> | ||||
|   <div class="notifications-link"> | ||||
|     <primary-nav-item | ||||
|       name="NOTIFICATIONS" | ||||
|       icon="alert" | ||||
|       :to="`/app/accounts/${accountId}/notifications`" | ||||
|       :count="unreadCount" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import PrimaryNavItem from 'dashboard/modules/sidebar/components/PrimaryNavItem'; | ||||
|  | ||||
| export default { | ||||
|   components: { PrimaryNavItem }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       accountId: 'getCurrentAccountId', | ||||
| @@ -15,40 +21,20 @@ export default { | ||||
|     }), | ||||
|     unreadCount() { | ||||
|       if (!this.notificationMetadata.unreadCount) { | ||||
|         return 0; | ||||
|         return '0'; | ||||
|       } | ||||
|  | ||||
|       return this.notificationMetadata.unreadCount < 100 | ||||
|         ? this.notificationMetadata.unreadCount | ||||
|         ? `${this.notificationMetadata.unreadCount}` | ||||
|         : '99+'; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     showNotification() { | ||||
|       this.$router.push(`/app/accounts/${this.accountId}/notifications`); | ||||
|     }, | ||||
|   }, | ||||
|   methods: {}, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .notifications { | ||||
|   font-size: var(--font-size-big); | ||||
|   margin-bottom: auto; | ||||
|   margin-left: auto; | ||||
|   margin-top: auto; | ||||
|   position: relative; | ||||
|  | ||||
|   .unread-badge { | ||||
|     background: var(--r-300); | ||||
|     border-radius: var(--space-small); | ||||
|     color: var(--white); | ||||
|     font-size: var(--font-size-micro); | ||||
|     font-weight: var(--font-weight-black); | ||||
|     left: var(--space-slab); | ||||
|     padding: 0 var(--space-smaller); | ||||
|     position: absolute; | ||||
|     top: var(--space-smaller); | ||||
|   } | ||||
| .notifications-link { | ||||
|   margin-bottom: var(--space-small); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,15 +2,19 @@ | ||||
|   <transition name="menu-slide"> | ||||
|     <div | ||||
|       v-if="show" | ||||
|       v-on-clickaway="() => $emit('close')" | ||||
|       class="dropdown-pane dropdowm--top" | ||||
|       v-on-clickaway="onClickAway" | ||||
|       class="dropdown-pane" | ||||
|       :class="{ 'dropdown-pane--open': show }" | ||||
|     > | ||||
|       <availability-status /> | ||||
|       <li class="divider" /> | ||||
|       <woot-dropdown-menu> | ||||
|         <woot-dropdown-item v-if="showChangeAccountOption"> | ||||
|           <woot-button | ||||
|             variant="clear" | ||||
|             color-scheme="secondary" | ||||
|             size="small" | ||||
|             class=" change-accounts--button" | ||||
|             icon="arrow-swap" | ||||
|             @click="$emit('toggle-accounts')" | ||||
|           > | ||||
|             {{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }} | ||||
| @@ -19,36 +23,50 @@ | ||||
|         <woot-dropdown-item v-if="globalConfig.chatwootInboxToken"> | ||||
|           <woot-button | ||||
|             variant="clear" | ||||
|             color-scheme="secondary" | ||||
|             size="small" | ||||
|             class=" change-accounts--button" | ||||
|             icon="ion-help-buoy" | ||||
|             @click="$emit('show-support-chat-window')" | ||||
|           > | ||||
|             Contact Support | ||||
|             {{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }} | ||||
|           </woot-button> | ||||
|         </woot-dropdown-item> | ||||
|         <woot-dropdown-item> | ||||
|           <woot-button | ||||
|             variant="clear" | ||||
|             color-scheme="secondary" | ||||
|             size="small" | ||||
|             class=" change-accounts--button" | ||||
|             @click="$emit('key-shortcut-modal')" | ||||
|             icon="keyboard" | ||||
|             @click="handleKeyboardHelpClick" | ||||
|           > | ||||
|             {{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }} | ||||
|           </woot-button> | ||||
|         </woot-dropdown-item> | ||||
|         <woot-dropdown-item> | ||||
|           <router-link | ||||
|             v-slot="{ href, isActive, navigate }" | ||||
|             :to="`/app/accounts/${accountId}/profile/settings`" | ||||
|             class="button clear small change-accounts--button" | ||||
|             custom | ||||
|           > | ||||
|             {{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }} | ||||
|             <a | ||||
|               :href="href" | ||||
|               class="button small clear secondary" | ||||
|               :class="{ 'is-active': isActive }" | ||||
|               @click="e => handleProfileSettingClick(e, navigate)" | ||||
|             > | ||||
|               <fluent-icon icon="person" class="icon icon--font" /> | ||||
|               <span class="button__content"> | ||||
|                 {{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }} | ||||
|               </span> | ||||
|             </a> | ||||
|           </router-link> | ||||
|         </woot-dropdown-item> | ||||
|         <woot-dropdown-item> | ||||
|           <woot-button | ||||
|             variant="clear" | ||||
|             color-scheme="secondary" | ||||
|             size="small" | ||||
|             class=" change-accounts--button" | ||||
|             icon="power" | ||||
|             @click="logout" | ||||
|           > | ||||
|             {{ $t('SIDEBAR_ITEMS.LOGOUT') }} | ||||
| @@ -63,13 +81,15 @@ | ||||
| import { mixin as clickaway } from 'vue-clickaway'; | ||||
| import { mapGetters } from 'vuex'; | ||||
| import Auth from '../../../api/auth'; | ||||
| import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; | ||||
| import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; | ||||
| import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     WootDropdownMenu, | ||||
|     WootDropdownItem, | ||||
|     AvailabilityStatus, | ||||
|   }, | ||||
|   mixins: [clickaway], | ||||
|   props: { | ||||
| @@ -88,18 +108,34 @@ export default { | ||||
|       if (this.globalConfig.createNewAccountFromDashboard) { | ||||
|         return true; | ||||
|       } | ||||
|       return this.currentUser.accounts.length > 1; | ||||
|  | ||||
|       const { accounts = [] } = this.currentUser; | ||||
|       return accounts.length > 1; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     handleProfileSettingClick(e, navigate) { | ||||
|       this.$emit('close'); | ||||
|       navigate(e); | ||||
|     }, | ||||
|     handleKeyboardHelpClick() { | ||||
|       this.$emit('key-shortcut-modal'); | ||||
|       this.$emit('close'); | ||||
|     }, | ||||
|     logout() { | ||||
|       Auth.logout(); | ||||
|     }, | ||||
|     onClickAway() { | ||||
|       if (this.show) this.$emit('close'); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .dropdown-pane { | ||||
|   right: 0; | ||||
|   left: var(--space-slab); | ||||
|   bottom: var(--space-larger); | ||||
|   min-width: 16.8rem; | ||||
|   z-index: var(--z-index-much-higher); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,13 +2,20 @@ import AgentDetails from '../AgentDetails'; | ||||
| 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'; | ||||
| import WootButton from 'dashboard/components/ui/WootButton'; | ||||
| 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', | ||||
| @@ -16,7 +23,11 @@ const i18nConfig = new VueI18n({ | ||||
| }); | ||||
|  | ||||
| describe('agentDetails', () => { | ||||
|   const currentUser = { name: 'Neymar Junior', avatar_url: '' }; | ||||
|   const currentUser = { | ||||
|     name: 'Neymar Junior', | ||||
|     avatar_url: '', | ||||
|     availability_status: 'online', | ||||
|   }; | ||||
|   const currentRole = 'agent'; | ||||
|   let store = null; | ||||
|   let actions = null; | ||||
| @@ -31,6 +42,7 @@ describe('agentDetails', () => { | ||||
|         getters: { | ||||
|           getCurrentUser: () => currentUser, | ||||
|           getCurrentRole: () => currentRole, | ||||
|           getCurrentUserAvailability: () => currentUser.availability_status, | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
| @@ -47,14 +59,8 @@ describe('agentDetails', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('shows the agent name', () => { | ||||
|     const agentTitle = agentDetails.find('.current-user--name'); | ||||
|     expect(agentTitle.text()).toBe('Neymar Junior'); | ||||
|   }); | ||||
|  | ||||
|   it('shows the agent role', () => { | ||||
|     const agentTitle = agentDetails.find('.current-user--role'); | ||||
|     expect(agentTitle.text()).toBe('Agent'); | ||||
|   it(' the agent status', () => { | ||||
|     expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online'); | ||||
|   }); | ||||
|  | ||||
|   it('agent thumbnail exists', () => { | ||||
|   | ||||
| @@ -50,8 +50,9 @@ describe('notificationBell', () => { | ||||
|       localVue, | ||||
|       i18n: i18nConfig, | ||||
|     }); | ||||
|     const statusViewTitle = notificationBell.find('.unread-badge'); | ||||
|     expect(statusViewTitle.text()).toBe('19'); | ||||
|  | ||||
|     const statusViewTitle = notificationBell.find('primary-nav-item-stub'); | ||||
|     expect(statusViewTitle.vm.count).toBe('19'); | ||||
|   }); | ||||
|  | ||||
|   it('it should return unread count 99+ ', async () => { | ||||
| @@ -61,7 +62,7 @@ describe('notificationBell', () => { | ||||
|       localVue, | ||||
|       i18n: i18nConfig, | ||||
|     }); | ||||
|     const statusViewTitle = notificationBell.find('.unread-badge'); | ||||
|     expect(statusViewTitle.text()).toBe('99+'); | ||||
|     const statusViewTitle = notificationBell.find('primary-nav-item-stub'); | ||||
|     expect(statusViewTitle.vm.count).toBe('99+'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,15 +1,23 @@ | ||||
| import AvailabilityStatus from '../AvailabilityStatus'; | ||||
| import AvailabilityStatus from '../AvailabilityStatus.vue'; | ||||
| import { createLocalVue, mount } from '@vue/test-utils'; | ||||
| import Vuex from 'vuex'; | ||||
| import VueI18n from 'vue-i18n'; | ||||
|  | ||||
| import WootButton from 'dashboard/components/ui/WootButton'; | ||||
| import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; | ||||
| import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; | ||||
| import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider'; | ||||
| import i18n from 'dashboard/i18n'; | ||||
|  | ||||
| const localVue = createLocalVue(); | ||||
| 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); | ||||
|  | ||||
| const i18nConfig = new VueI18n({ | ||||
|   locale: 'en', | ||||
| @@ -52,29 +60,11 @@ describe('AvailabilityStatus', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('shows current user status', () => { | ||||
|     const statusViewTitle = availabilityStatus.find('.status-view--title'); | ||||
|  | ||||
|     expect(statusViewTitle.text()).toBe('Online'); | ||||
|   }); | ||||
|  | ||||
|   it('opens the menu when user clicks "change"', async () => { | ||||
|     expect(availabilityStatus.find('.dropdown-pane').exists()).toBe(false); | ||||
|  | ||||
|     await availabilityStatus | ||||
|       .find('.status-change--change-button') | ||||
|       .trigger('click'); | ||||
|  | ||||
|     expect(availabilityStatus.find('.dropdown-pane').exists()).toBe(true); | ||||
|   }); | ||||
|  | ||||
|   it('dispatches an action when user changes status', async () => { | ||||
|     await availabilityStatus | ||||
|       .find('.status-change--change-button') | ||||
|       .trigger('click'); | ||||
|  | ||||
|     await availabilityStatus | ||||
|       .find('.status-change li:last-child button') | ||||
|     await availabilityStatus; | ||||
|     availabilityStatus | ||||
|       .findAll('.status-change--dropdown-button') | ||||
|       .at(2) | ||||
|       .trigger('click'); | ||||
|  | ||||
|     expect(actions.updateAvailability).toBeCalledWith( | ||||
|   | ||||
| @@ -116,6 +116,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     shouldShowStatusAlways: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @@ -124,6 +128,7 @@ export default { | ||||
|   }, | ||||
|   computed: { | ||||
|     showStatusIndicator() { | ||||
|       if (this.shouldShowStatusAlways) return true; | ||||
|       return this.status === 'online' || this.status === 'busy'; | ||||
|     }, | ||||
|     avatarSize() { | ||||
| @@ -210,5 +215,9 @@ export default { | ||||
|   .user-online-status--busy { | ||||
|     background: $warning-color; | ||||
|   } | ||||
|  | ||||
|   .user-online-status--offline { | ||||
|     background: var(--s-500); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export default { | ||||
|     background: var(--g-400); | ||||
|   } | ||||
|   &__offline { | ||||
|     background: var(--b-600); | ||||
|     background: var(--s-500); | ||||
|   } | ||||
|   &__busy { | ||||
|     background: var(--y-700); | ||||
|   | ||||
| @@ -51,6 +51,7 @@ | ||||
|         @toggle-user-mention="toggleUserMention" | ||||
|         @toggle-canned-menu="toggleCannedMenu" | ||||
|       /> | ||||
|       <h1>{{ message }}</h1> | ||||
|     </div> | ||||
|     <div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste"> | ||||
|       <attachment-preview | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| <template> | ||||
|   <div class="file message-text__wrap"> | ||||
|     <div class="icon-wrap"> | ||||
|       <fluent-icon icon="document" class="file--icon" /> | ||||
|       <fluent-icon icon="document" class="file--icon" size="32" /> | ||||
|     </div> | ||||
|     <div class="meta"> | ||||
|       <h5 class="text-block-title"> | ||||
|         {{ decodeURI(fileName) }} | ||||
|       </h5> | ||||
|       <a | ||||
|         class="download clear button small" | ||||
|         class="download clear link button small" | ||||
|         rel="noreferrer noopener nofollow" | ||||
|         target="_blank" | ||||
|         :href="url" | ||||
|   | ||||
| @@ -3,6 +3,9 @@ import contacts from './sidebarItems/contacts'; | ||||
| import reports from './sidebarItems/reports'; | ||||
| import campaigns from './sidebarItems/campaigns'; | ||||
| import settings from './sidebarItems/settings'; | ||||
| import notifications from './sidebarItems/notifications'; | ||||
|  | ||||
| // TODO - find hasSubMenu usage - July/2021 | ||||
|  | ||||
| export const getSidebarItems = accountId => ({ | ||||
|   common: common(accountId), | ||||
| @@ -10,4 +13,5 @@ export const getSidebarItems = accountId => ({ | ||||
|   reports: reports(accountId), | ||||
|   campaigns: campaigns(accountId), | ||||
|   settings: settings(accountId), | ||||
|   notifications: notifications(accountId), | ||||
| }); | ||||
|   | ||||
| @@ -103,6 +103,7 @@ | ||||
|   "SIDEBAR_ITEMS": { | ||||
|     "CHANGE_AVAILABILITY_STATUS": "Change", | ||||
|     "CHANGE_ACCOUNTS": "Switch Account", | ||||
|     "CONTACT_SUPPORT": "Contact Support", | ||||
|     "SELECTOR_SUBTITLE": "Select an account from the following list", | ||||
|     "PROFILE_SETTINGS": "Profile Settings", | ||||
|     "KEYBOARD_SHORTCUTS": "Keyboard Shortcuts", | ||||
| @@ -143,6 +144,7 @@ | ||||
|     "NOTIFICATIONS": "Notifications", | ||||
|     "CANNED_RESPONSES": "Canned Responses", | ||||
|     "INTEGRATIONS": "Integrations", | ||||
|     "PROFILE_SETTINGS": "Profile Settings", | ||||
|     "ACCOUNT_SETTINGS": "Account Settings", | ||||
|     "APPLICATIONS": "Applications", | ||||
|     "LABELS": "Labels", | ||||
| @@ -151,6 +153,9 @@ | ||||
|     "TEAMS": "Teams", | ||||
|     "ALL_CONTACTS": "All Contacts", | ||||
|     "TAGGED_WITH": "Tagged with", | ||||
|     "NEW_LABEL": "New label", | ||||
|     "NEW_TEAM": "New team", | ||||
|     "NEW_INBOX": "New inbox", | ||||
|     "REPORTS_OVERVIEW": "Overview", | ||||
|     "CSAT": "CSAT", | ||||
|     "CAMPAIGNS": "Campaigns", | ||||
| @@ -159,7 +164,8 @@ | ||||
|     "REPORTS_AGENT": "Agents", | ||||
|     "REPORTS_LABEL": "Labels", | ||||
|     "REPORTS_INBOX": "Inbox", | ||||
|     "REPORTS_TEAM": "Team" | ||||
|     "REPORTS_TEAM": "Team", | ||||
|     "SET_AVAILABILITY_TITLE": "Set yourself as" | ||||
|   }, | ||||
|   "CREATE_ACCOUNT": { | ||||
|     "NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.", | ||||
|   | ||||
| @@ -2,29 +2,24 @@ import { frontendURL } from '../../helper/URLHelper'; | ||||
|  | ||||
| const campaigns = accountId => ({ | ||||
|   routes: ['settings_account_campaigns', 'one_off'], | ||||
|   menuItems: { | ||||
|     back: { | ||||
|       icon: 'chevron-left', | ||||
|       label: 'HOME', | ||||
|       hasSubMenu: false, | ||||
|       toStateName: 'home', | ||||
|       toState: frontendURL(`accounts/${accountId}/dashboard`), | ||||
|     }, | ||||
|     ongoingCampaigns: { | ||||
|   menuItems: [ | ||||
|     { | ||||
|       icon: 'arrow-swap', | ||||
|       label: 'ONGOING', | ||||
|       key: 'ongoingCampaigns', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`), | ||||
|       toStateName: 'settings_account_campaigns', | ||||
|     }, | ||||
|     onOffCampaigns: { | ||||
|     { | ||||
|       key: 'oneOffCampaigns', | ||||
|       icon: 'sound-source', | ||||
|       label: 'ONE_OFF', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/campaigns/one_off`), | ||||
|       toStateName: 'one_off', | ||||
|     }, | ||||
|   }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| export default campaigns; | ||||
|   | ||||
| @@ -19,44 +19,41 @@ const common = accountId => ({ | ||||
|     assignedToMe: { | ||||
|       icon: 'chat', | ||||
|       label: 'CONVERSATIONS', | ||||
|       hasSubMenu: false, | ||||
|       key: '', | ||||
|       hasSubMenu: true, | ||||
|       key: 'conversations', | ||||
|       toState: frontendURL(`accounts/${accountId}/dashboard`), | ||||
|       toolTip: 'Conversation from all subscribed inboxes', | ||||
|       toStateName: 'home', | ||||
|     }, | ||||
|     contacts: { | ||||
|       key: 'contacts', | ||||
|       icon: 'book-contacts', | ||||
|       label: 'CONTACTS', | ||||
|       hasSubMenu: false, | ||||
|       hasSubMenu: true, | ||||
|       toState: frontendURL(`accounts/${accountId}/contacts`), | ||||
|       toStateName: 'contacts_dashboard', | ||||
|     }, | ||||
|     notifications: { | ||||
|       icon: 'alert', | ||||
|       label: 'NOTIFICATIONS', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/notifications`), | ||||
|       toStateName: 'notifications_dashboard', | ||||
|     }, | ||||
|     report: { | ||||
|     reports: { | ||||
|       key: 'reports', | ||||
|       icon: 'arrow-trending-lines', | ||||
|       label: 'REPORTS', | ||||
|       hasSubMenu: false, | ||||
|       hasSubMenu: true, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports`), | ||||
|       toStateName: 'settings_account_reports', | ||||
|     }, | ||||
|     campaigns: { | ||||
|       key: 'campaigns', | ||||
|       icon: 'megaphone', | ||||
|       label: 'CAMPAIGNS', | ||||
|       hasSubMenu: false, | ||||
|       hasSubMenu: true, | ||||
|       toState: frontendURL(`accounts/${accountId}/campaigns`), | ||||
|       toStateName: 'settings_account_campaigns', | ||||
|     }, | ||||
|     settings: { | ||||
|       key: 'settings', | ||||
|       icon: 'settings', | ||||
|       label: 'SETTINGS', | ||||
|       hasSubMenu: false, | ||||
|       hasSubMenu: true, | ||||
|       toState: frontendURL(`accounts/${accountId}/settings`), | ||||
|       toStateName: 'settings_home', | ||||
|     }, | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| const notifications = () => ({ | ||||
|   routes: ['notifications_index'], | ||||
|   menuItems: {}, | ||||
| }); | ||||
|  | ||||
| export default notifications; | ||||
| @@ -9,57 +9,50 @@ const reports = accountId => ({ | ||||
|     'inbox_reports', | ||||
|     'team_reports', | ||||
|   ], | ||||
|   menuItems: { | ||||
|     back: { | ||||
|       icon: 'chevron-left', | ||||
|       label: 'HOME', | ||||
|       hasSubMenu: false, | ||||
|       toStateName: 'home', | ||||
|       toState: frontendURL(`accounts/${accountId}/dashboard`), | ||||
|     }, | ||||
|     reportOverview: { | ||||
|   menuItems: [ | ||||
|     { | ||||
|       icon: 'arrow-trending-lines', | ||||
|       label: 'REPORTS_OVERVIEW', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/overview`), | ||||
|       toStateName: 'settings_account_reports', | ||||
|     }, | ||||
|     csatReports: { | ||||
|     { | ||||
|       icon: 'emoji', | ||||
|       label: 'CSAT', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/csat`), | ||||
|       toStateName: 'csat_reports', | ||||
|     }, | ||||
|     agentReports: { | ||||
|     { | ||||
|       icon: 'people', | ||||
|       label: 'REPORTS_AGENT', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/agent`), | ||||
|       toStateName: 'agent_reports', | ||||
|     }, | ||||
|     labelReports: { | ||||
|     { | ||||
|       icon: 'tag', | ||||
|       label: 'REPORTS_LABEL', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/label`), | ||||
|       toStateName: 'label_reports', | ||||
|     }, | ||||
|     inboxReports: { | ||||
|     { | ||||
|       icon: 'mail-inbox-all', | ||||
|       label: 'REPORTS_INBOX', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/inboxes`), | ||||
|       toStateName: 'inbox_reports', | ||||
|     }, | ||||
|     teamReports: { | ||||
|     { | ||||
|       icon: 'people-team', | ||||
|       label: 'REPORTS_TEAM', | ||||
|       hasSubMenu: false, | ||||
|       toState: frontendURL(`accounts/${accountId}/reports/teams`), | ||||
|       toStateName: 'team_reports', | ||||
|     }, | ||||
|   }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| export default reports; | ||||
|   | ||||
							
								
								
									
										46
									
								
								app/javascript/dashboard/modules/sidebar/components/Logo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/javascript/dashboard/modules/sidebar/components/Logo.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
|   <div class="logo"> | ||||
|     <router-link :to="dashboardPath" replace> | ||||
|       <img :src="source" :alt="name" /> | ||||
|     </router-link> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { frontendURL } from 'dashboard/helper/URLHelper'; | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     source: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     name: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     accountId: { | ||||
|       type: Number, | ||||
|       default: 0, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     dashboardPath() { | ||||
|       return frontendURL(`accounts/${this.accountId}/dashboard`); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| $logo-size: 32px; | ||||
|  | ||||
| .logo { | ||||
|   padding: var(--space-normal); | ||||
|  | ||||
|   img { | ||||
|     width: $logo-size; | ||||
|     height: $logo-size; | ||||
|     object-fit: cover; | ||||
|     object-position: left center; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										120
									
								
								app/javascript/dashboard/modules/sidebar/components/Primary.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								app/javascript/dashboard/modules/sidebar/components/Primary.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| <template> | ||||
|   <div class="primary--sidebar"> | ||||
|     <logo | ||||
|       :source="logoSource" | ||||
|       :name="installationName" | ||||
|       :account-id="accountId" | ||||
|     /> | ||||
|     <nav class="menu vertical"> | ||||
|       <primary-nav-item | ||||
|         v-for="menuItem in menuItems" | ||||
|         :key="menuItem.toState" | ||||
|         :icon="menuItem.icon" | ||||
|         :name="menuItem.label" | ||||
|         :to="menuItem.toState" | ||||
|         :is-child-menu-active="isMenuActive(menuItem, $route.name)" | ||||
|       /> | ||||
|     </nav> | ||||
|     <div class="menu vertical user-menu"> | ||||
|       <notification-bell /> | ||||
|       <agent-details @toggle-menu="toggleOptions" /> | ||||
|       <options-menu | ||||
|         :show="showOptionsMenu" | ||||
|         @toggle-accounts="toggleAccountModal" | ||||
|         @show-support-chat-window="toggleSupportChatWindow" | ||||
|         @key-shortcut-modal="$emit('key-shortcut-modal')" | ||||
|         @close="toggleOptions" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import Logo from './Logo'; | ||||
| import PrimaryNavItem from './PrimaryNavItem'; | ||||
| import OptionsMenu from 'dashboard/components/layout/sidebarComponents/OptionsMenu'; | ||||
| import AgentDetails from 'dashboard/components/layout/sidebarComponents/AgentDetails'; | ||||
| import NotificationBell from 'dashboard/components/layout/sidebarComponents/NotificationBell'; | ||||
|  | ||||
| import { frontendURL } from 'dashboard/helper/URLHelper'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Logo, | ||||
|     PrimaryNavItem, | ||||
|     OptionsMenu, | ||||
|     AgentDetails, | ||||
|     NotificationBell, | ||||
|   }, | ||||
|   props: { | ||||
|     logoSource: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     installationName: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     accountId: { | ||||
|       type: Number, | ||||
|       default: 0, | ||||
|     }, | ||||
|     menuItems: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showOptionsMenu: false, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     frontendURL, | ||||
|     toggleOptions() { | ||||
|       this.showOptionsMenu = !this.showOptionsMenu; | ||||
|     }, | ||||
|     toggleAccountModal() { | ||||
|       this.$emit('toggle-accounts'); | ||||
|     }, | ||||
|     toggleSupportChatWindow() { | ||||
|       window.$chatwoot.toggle(); | ||||
|     }, | ||||
|     isMenuActive(menuItem, currentRouteName) { | ||||
|       const { key = '' } = menuItem; | ||||
|  | ||||
|       if (currentRouteName === key) return true; | ||||
|       // Conversations route is defaulted as home | ||||
|       // TODO: Needs to ewfactor old statenames to follow a structure while key naming. | ||||
|       if (currentRouteName.includes('inbox') && key === 'conversations') | ||||
|         return true; | ||||
|       if (currentRouteName.includes('conversations') && key === 'conversations') | ||||
|         return true; | ||||
|       return false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .primary--sidebar { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   width: var(--space-jumbo); | ||||
|   border-right: 1px solid var(--s-50); | ||||
|   box-sizing: content-box; | ||||
|   height: 100vh; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .menu { | ||||
|   align-items: center; | ||||
|   margin-top: var(--space-medium); | ||||
| } | ||||
|  | ||||
| .user-menu { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   flex-grow: 1; | ||||
|   justify-content: flex-end; | ||||
|   margin-bottom: var(--space-normal); | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,78 @@ | ||||
| <template> | ||||
|   <router-link v-slot="{ href, isActive, navigate }" :to="to" custom> | ||||
|     <a | ||||
|       v-tooltip.right="$t(`SIDEBAR.${name}`)" | ||||
|       :href="href" | ||||
|       class="button clear button--only-icon menu-item" | ||||
|       :class="{ 'is-active': isActive || isChildMenuActive }" | ||||
|       @click="navigate" | ||||
|     > | ||||
|       <fluent-icon :icon="icon" /> | ||||
|       <span class="show-for-sr">{{ name }}</span> | ||||
|       <span v-if="count" class="badge warning">{{ count }}</span> | ||||
|     </a> | ||||
|   </router-link> | ||||
| </template> | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     to: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     name: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     icon: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     count: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     isChildMenuActive: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .button { | ||||
|   margin: var(--space-small) 0; | ||||
| } | ||||
|  | ||||
| .menu-item { | ||||
|   display: inline-flex; | ||||
|   position: relative; | ||||
|   border-radius: var(--border-radius-large); | ||||
|   border: 1px solid transparent; | ||||
|   color: var(--s-600); | ||||
|  | ||||
|   &:hover { | ||||
|     background: var(--w-25); | ||||
|     color: var(--s-600); | ||||
|   } | ||||
|  | ||||
|   &:focus { | ||||
|     border-color: var(--w-500); | ||||
|   } | ||||
|  | ||||
|   &.is-active { | ||||
|     background: var(--w-50); | ||||
|     color: var(--w-500); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .icon { | ||||
|   font-size: var(--font-size-default); | ||||
| } | ||||
|  | ||||
| .badge { | ||||
|   position: absolute; | ||||
|   right: var(--space-minus-smaller); | ||||
|   top: var(--space-minus-smaller); | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,215 @@ | ||||
| <template> | ||||
|   <div class="main-nav secondary-menu"> | ||||
|     <transition-group name="menu-list" tag="ul" class="menu vertical"> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowConversationsSideMenu" | ||||
|         :key="inboxSection.toState" | ||||
|         :menu-item="inboxSection" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowTeamsSideMenu" | ||||
|         :key="teamSection.toState" | ||||
|         :menu-item="teamSection" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowConversationsSideMenu" | ||||
|         :key="labelSection.toState" | ||||
|         :menu-item="labelSection" | ||||
|         @add-label="showAddLabelPopup" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowContactSideMenu" | ||||
|         :key="contactLabelSection.key" | ||||
|         :menu-item="contactLabelSection" | ||||
|         @add-label="showAddLabelPopup" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowCampaignSideMenu" | ||||
|         :key="campaignSubSection.key" | ||||
|         :menu-item="campaignSubSection" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowReportsSideMenu" | ||||
|         :key="reportsSubSection.key" | ||||
|         :menu-item="reportsSubSection" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowSettingsSideMenu" | ||||
|         :key="settingsSubMenu.key" | ||||
|         :menu-item="settingsSubMenu" | ||||
|       /> | ||||
|       <sidebar-item | ||||
|         v-if="shouldShowNotificationsSideMenu" | ||||
|         :key="notificationsSubMenu.key" | ||||
|         :menu-item="notificationsSubMenu" | ||||
|       /> | ||||
|     </transition-group> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { frontendURL } from '../../../helper/URLHelper'; | ||||
| import SidebarItem from 'dashboard/components/layout/SidebarItem'; | ||||
| import routesMixin from 'dashboard/modules/sidebar/mixins/routes.mixin'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     SidebarItem, | ||||
|   }, | ||||
|   mixins: [routesMixin], | ||||
|   props: { | ||||
|     accountId: { | ||||
|       type: Number, | ||||
|       default: 0, | ||||
|     }, | ||||
|     accountLabels: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     inboxes: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     teams: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     menuItems: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     inboxSection() { | ||||
|       return { | ||||
|         icon: 'folder', | ||||
|         label: 'INBOXES', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         newLinkTag: 'NEW_INBOX', | ||||
|         key: 'inbox', | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`), | ||||
|         toStateName: 'settings_inbox_new', | ||||
|         newLinkRouteName: 'settings_inbox_new', | ||||
|         children: this.inboxes.map(inbox => ({ | ||||
|           id: inbox.id, | ||||
|           label: inbox.name, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`), | ||||
|           type: inbox.channel_type, | ||||
|           phoneNumber: inbox.phone_number, | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     labelSection() { | ||||
|       return { | ||||
|         icon: 'number-symbol', | ||||
|         label: 'LABELS', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         newLinkTag: 'NEW_LABEL', | ||||
|         key: 'label', | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/labels`), | ||||
|         toStateName: 'labels_list', | ||||
|         showModalForNewItem: true, | ||||
|         modalName: 'AddLabel', | ||||
|         children: this.accountLabels.map(label => ({ | ||||
|           id: label.id, | ||||
|           label: label.title, | ||||
|           color: label.color, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL( | ||||
|             `accounts/${this.accountId}/label/${label.title}` | ||||
|           ), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     contactLabelSection() { | ||||
|       return { | ||||
|         icon: 'number-symbol', | ||||
|         label: 'TAGGED_WITH', | ||||
|         hasSubMenu: true, | ||||
|         key: 'label', | ||||
|         newLink: true, | ||||
|         newLinkTag: 'NEW_LABEL', | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/labels`), | ||||
|         toStateName: 'labels_list', | ||||
|         showModalForNewItem: true, | ||||
|         modalName: 'AddLabel', | ||||
|         children: this.accountLabels.map(label => ({ | ||||
|           id: label.id, | ||||
|           label: label.title, | ||||
|           color: label.color, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL( | ||||
|             `accounts/${this.accountId}/labels/${label.title}/contacts` | ||||
|           ), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     campaignSubSection() { | ||||
|       return this.getSubSectionByKey('campaigns'); | ||||
|     }, | ||||
|     teamSection() { | ||||
|       return { | ||||
|         icon: 'people-team', | ||||
|         label: 'TEAMS', | ||||
|         hasSubMenu: true, | ||||
|         newLink: true, | ||||
|         newLinkTag: 'NEW_TEAM', | ||||
|         key: 'team', | ||||
|         cssClass: 'menu-title align-justify teams-sidebar-menu', | ||||
|         toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`), | ||||
|         toStateName: 'settings_teams_new', | ||||
|         newLinkRouteName: 'settings_teams_new', | ||||
|         children: this.teams.map(team => ({ | ||||
|           id: team.id, | ||||
|           label: team.name, | ||||
|           truncateLabel: true, | ||||
|           toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     notificationsSubMenu() { | ||||
|       return { | ||||
|         icon: 'alert', | ||||
|         label: 'NOTIFICATIONS', | ||||
|         hasSubMenu: false, | ||||
|         cssClass: 'menu-title align-justify', | ||||
|         key: 'notifications', | ||||
|         children: [], | ||||
|       }; | ||||
|     }, | ||||
|     settingsSubMenu() { | ||||
|       return this.getSubSectionByKey('settings'); | ||||
|     }, | ||||
|     reportsSubSection() { | ||||
|       return this.getSubSectionByKey('reports'); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     getSubSectionByKey(subSectionKey) { | ||||
|       const menuItems = Object.values( | ||||
|         this.sideMenuItems[subSectionKey].menuItems | ||||
|       ); | ||||
|       const campaignItem = this.menuItems.find( | ||||
|         ({ key }) => key === subSectionKey | ||||
|       ); | ||||
|  | ||||
|       return { | ||||
|         ...campaignItem, | ||||
|         children: menuItems.map(item => ({ | ||||
|           ...item, | ||||
|           label: this.$t(`SIDEBAR.${item.label}`), | ||||
|         })), | ||||
|       }; | ||||
|     }, | ||||
|     showAddLabelPopup() { | ||||
|       this.$emit('add-label'); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -0,0 +1,138 @@ | ||||
| <template> | ||||
|   <router-link | ||||
|     v-slot="{ href, isActive, navigate }" | ||||
|     :to="to" | ||||
|     custom | ||||
|     active-class="active" | ||||
|   > | ||||
|     <li :class="{ active: isActive }"> | ||||
|       <a | ||||
|         :href="href" | ||||
|         class="button clear menu-item text-truncate" | ||||
|         :class="{ 'is-active': isActive, 'text-truncate': shouldTruncate }" | ||||
|         @click="navigate" | ||||
|       > | ||||
|         <span v-if="icon" class="badge--icon"> | ||||
|           <fluent-icon class="inbox-icon" :icon="icon" size="10" /> | ||||
|         </span> | ||||
|         <span | ||||
|           v-if="labelColor" | ||||
|           class="badge--label" | ||||
|           :style="{ backgroundColor: labelColor }" | ||||
|         /> | ||||
|         <span | ||||
|           :title="menuTitle" | ||||
|           class="menu-label button__content" | ||||
|           :class="{ 'text-truncate': shouldTruncate }" | ||||
|         > | ||||
|           {{ label }} | ||||
|         </span> | ||||
|         <span v-if="count" class="badge" :class="{ secondary: !isActive }"> | ||||
|           {{ count }} | ||||
|         </span> | ||||
|       </a> | ||||
|     </li> | ||||
|   </router-link> | ||||
| </template> | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     to: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     label: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     labelColor: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     shouldTruncate: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     icon: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     count: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     showIcon() { | ||||
|       return { 'text-truncate': this.shouldTruncate }; | ||||
|     }, | ||||
|     menuTitle() { | ||||
|       return this.shouldTruncate ? this.label : ''; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| $badge-size: var(--space-slab); | ||||
|  | ||||
| .button { | ||||
|   margin: var(--space-small) 0; | ||||
| } | ||||
|  | ||||
| .menu-item { | ||||
|   display: inline-flex; | ||||
|   color: var(--s-600); | ||||
|   font-weight: var(--font-weight-medium); | ||||
|   width: 100%; | ||||
|   height: var(--space-medium); | ||||
|   padding: var(--space-smaller) var(--space-smaller); | ||||
|   margin: var(--space-smaller) 0; | ||||
|   text-align: left; | ||||
|  | ||||
|   &:hover { | ||||
|     background: var(--s-25); | ||||
|     color: var(--s-600); | ||||
|   } | ||||
|  | ||||
|   &:focus { | ||||
|     border-color: var(--w-300); | ||||
|   } | ||||
|  | ||||
|   &.is-active { | ||||
|     background: var(--w-25); | ||||
|     color: var(--w-500); | ||||
|     border-color: var(--w-25); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .menu-label { | ||||
|   flex-grow: 1; | ||||
|   line-height: var(--space-two); | ||||
| } | ||||
|  | ||||
| .inbox-icon { | ||||
|   font-size: var(--font-size-nano); | ||||
| } | ||||
|  | ||||
| .badge--label, | ||||
| .badge--icon { | ||||
|   display: inline-flex; | ||||
|   min-width: $badge-size; | ||||
|   height: $badge-size; | ||||
|   border-radius: var(--border-radius-small); | ||||
|   margin-right: var(--space-smaller); | ||||
|   background: var(--s-100); | ||||
| } | ||||
|  | ||||
| .badge--icon { | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .badge.secondary { | ||||
|   min-width: unset; | ||||
|   background: var(--s-75); | ||||
|   color: var(--s-600); | ||||
|   font-weight: var(--font-weight-bold); | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,35 @@ | ||||
| import { getSidebarItems } from 'dashboard/i18n/default-sidebar'; | ||||
|  | ||||
| export default { | ||||
|   computed: { | ||||
|     currentRoute() { | ||||
|       return this.$store.state.route.name; | ||||
|     }, | ||||
|     sideMenuItems() { | ||||
|       return getSidebarItems(this.accountId); | ||||
|     }, | ||||
|     shouldShowConversationsSideMenu() { | ||||
|       return this.sideMenuItems.common.routes.includes(this.currentRoute); | ||||
|     }, | ||||
|     shouldShowContactSideMenu() { | ||||
|       return this.sideMenuItems.contacts.routes.includes(this.currentRoute); | ||||
|     }, | ||||
|     shouldShowCampaignSideMenu() { | ||||
|       return this.sideMenuItems.campaigns.routes.includes(this.currentRoute); | ||||
|     }, | ||||
|     shouldShowSettingsSideMenu() { | ||||
|       return this.sideMenuItems.settings.routes.includes(this.currentRoute); | ||||
|     }, | ||||
|     shouldShowReportsSideMenu() { | ||||
|       return this.sideMenuItems.reports.routes.includes(this.currentRoute); | ||||
|     }, | ||||
|     shouldShowNotificationsSideMenu() { | ||||
|       return this.sideMenuItems.notifications.routes.includes( | ||||
|         this.currentRoute | ||||
|       ); | ||||
|     }, | ||||
|     shouldShowTeamsSideMenu() { | ||||
|       return this.shouldShowConversationsSideMenu && this.teams.length; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| @@ -33,9 +33,9 @@ export default { | ||||
|         return ''; | ||||
|       } | ||||
|       if (this.isSidebarOpen) { | ||||
|         return 'off-canvas is-open '; | ||||
|         return 'off-canvas position-left is-transition-push is-open'; | ||||
|       } | ||||
|       return 'off-canvas position-left is-transition-push is-closed'; | ||||
|       return 'off-canvas is-transition-push is-closed'; | ||||
|     }, | ||||
|     contentClassName() { | ||||
|       if (this.isOnDesktop) { | ||||
| @@ -44,7 +44,7 @@ export default { | ||||
|       if (this.isSidebarOpen) { | ||||
|         return 'off-canvas-content is-open-left has-transition-push has-position-left'; | ||||
|       } | ||||
|       return 'off-canvas-content'; | ||||
|       return 'off-canvas-content has-transition-push'; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
| @@ -71,3 +71,8 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .off-canvas-content.is-open-left { | ||||
|   transform: translateX(25.4rem); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,16 +2,17 @@ | ||||
|   --white: #fff; | ||||
|   --white-transparent: rgba(255, 255, 255, 0.9); | ||||
|  | ||||
|   --w-50: #E3F2FF; | ||||
|   --w-100: #BBDDFF; | ||||
|   --w-200: #8FC9FF; | ||||
|   --w-300: #61B3FF; | ||||
|   --w-400: #3FA3FF; | ||||
|   --w-25: #F5FAFF; | ||||
|   --w-50: #EBF5FF; | ||||
|   --w-100: #C2E1FF; | ||||
|   --w-200: #99CEFF; | ||||
|   --w-300: ##70BAFF; | ||||
|   --w-400: #47A6FF; | ||||
|   --w-500: #1F93FF; | ||||
|   --w-600: #2284F0; | ||||
|   --w-700: #2272DC; | ||||
|   --w-800: #2161CA; | ||||
|   --w-900: #1F41AB; | ||||
|   --w-600: #1976CC; | ||||
|   --w-700: #135899; | ||||
|   --w-800: #0C3B66; | ||||
|   --w-900: #061D33; | ||||
|  | ||||
|   --g-50: #E6F8E6; | ||||
|   --g-100: #C4EEC2; | ||||
| @@ -35,16 +36,18 @@ | ||||
|   --y-800: #FDAD2A; | ||||
|   --y-900: #F9841B; | ||||
|  | ||||
|   --s-50: #E7EEFB; | ||||
|   --s-100: #C8D6E6; | ||||
|   --s-200: #ABBACE; | ||||
|   --s-300: #8C9EB6; | ||||
|   --s-400: #7489A4; | ||||
|   --s-500: #5D7592; | ||||
|   --s-600: #506781; | ||||
|   --s-700: #40546B; | ||||
|   --s-800: #314155; | ||||
|   --s-900: #1F2D3D; | ||||
|   --s-25: #F8FAFC; | ||||
|   --s-50: #F1F5F8; | ||||
|   --s-75: #EBF0F5; | ||||
|   --s-100: #E4EBF1; | ||||
|   --s-200: #C9D7E3; | ||||
|   --s-300: #AEC3D5; | ||||
|   --s-400: #93AFC8; | ||||
|   --s-500: #779BBB; | ||||
|   --s-600: #446888; | ||||
|   --s-700: #37546D; | ||||
|   --s-800: #293F51; | ||||
|   --s-900: #1B2836; | ||||
|  | ||||
|   --b-50: #F8F9FE; | ||||
|   --b-100: #F2F3F7; | ||||
| @@ -85,12 +88,12 @@ | ||||
|   --color-heading: #1f2d3d; | ||||
|   --color-body: #3c4858; | ||||
|  | ||||
|   --color-border: #e0e6ed; | ||||
|   --color-border-light: #f0f4f5; | ||||
|   --color-border-dark: #cad0d4; | ||||
|   --color-border: var(--s-75); | ||||
|   --color-border-light: var(--s-50); | ||||
|   --color-border-dark: var(--s-100); | ||||
|  | ||||
|   --color-background: #f4f6fb; | ||||
|   --color-background-light: #f9fafc; | ||||
|   --color-background: var(--s-50); | ||||
|   --color-background-light: var(--s-25); | ||||
|  | ||||
|   // Social and inboxes brand colors | ||||
|   --color-facebook-brand: #3b5998; | ||||
|   | ||||
| @@ -41,6 +41,7 @@ | ||||
|   "headphones-sound-wave-outline": "M3.5 12a8.5 8.5 0 0 1 17 0v2h-2.25a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75H19a3 3 0 0 0 3-3v-7c0-5.523-4.477-10-10-10S2 6.477 2 12v7a3 3 0 0 0 3 3h.75a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75H3.5v-2Zm17 3.5V19a1.5 1.5 0 0 1-1.5 1.5v-5h1.5ZM3.5 19v-3.5H5v5A1.5 1.5 0 0 1 3.5 19Zm9.25-7.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5Zm-4 2.25a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5a.75.75 0 0 1 .75-.75Zm7.25.75a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0v-4.5Z", | ||||
|   "image-outline": "M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm.58 16.401-5.805-5.686a.75.75 0 0 0-.966-.071l-.084.07-5.807 5.687c.182.064.378.099.582.099h11.5c.203 0 .399-.035.58-.099l-5.805-5.686L18.33 19.4ZM17.75 4.5H6.25A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .208.036.408.103.594l5.823-5.701a2.25 2.25 0 0 1 3.02-.116l.128.116 5.822 5.702c.067-.186.104-.386.104-.595V6.25a1.75 1.75 0 0 0-1.75-1.75Zm-2.498 2a2.252 2.252 0 1 1 0 4.504 2.252 2.252 0 0 1 0-4.504Zm0 1.5a.752.752 0 1 0 0 1.504.752.752 0 0 0 0-1.504Z", | ||||
|   "info-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5Zm-.004 7a.75.75 0 0 1 .744.648l.007.102.003 5.502a.75.75 0 0 1-1.493.102l-.007-.101-.003-5.502a.75.75 0 0 1 .75-.75ZM12 7.003a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997Z", | ||||
|   "keyboard-outline": "M19.745 5a2.25 2.25 0 0 1 2.25 2.25v9.505a2.25 2.25 0 0 1-2.25 2.25H4.25A2.25 2.25 0 0 1 2 16.755V7.25A2.25 2.25 0 0 1 4.25 5h15.495Zm0 1.5H4.25a.75.75 0 0 0-.75.75v9.505c0 .414.336.75.75.75h15.495a.75.75 0 0 0 .75-.75V7.25a.75.75 0 0 0-.75-.75Zm-12.995 8h10.5a.75.75 0 0 1 .102 1.493L17.25 16H6.75a.75.75 0 0 1-.102-1.493l.102-.007h10.5-10.5ZM16.5 11a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-5.995 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm6 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2ZM6 8a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm2.995 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z", | ||||
|   "link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z", | ||||
|   "list-outline": "M2.75 18h12.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.494L2.75 18h12.5-12.5Zm0-6.5h18.5a.75.75 0 0 1 .102 1.493L21.25 13H2.75a.75.75 0 0 1-.102-1.493l.102-.007h18.5-18.5Zm0-6.497h15.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.493l.102-.007h15.5-15.5Z", | ||||
|   "location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z", | ||||
| @@ -57,6 +58,7 @@ | ||||
|   "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", | ||||
|   "people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z", | ||||
|   "person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z", | ||||
|   "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", | ||||
|   "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z", | ||||
|   "resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z", | ||||
|   "search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z", | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| <template> | ||||
|   <li | ||||
|     class="dropdown-menu--divider" | ||||
|     :tabindex="null" | ||||
|     :aria-disabled="true" | ||||
|   ></li> | ||||
| </template> | ||||
| <script> | ||||
| export default {}; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .dropdown-menu--divider { | ||||
|   list-style: none; | ||||
|   margin: var(--space-small) 0; | ||||
|   border-bottom: 1px solid var(--s-50); | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
|   <li class="dropdown-menu--header" :tabindex="null" :aria-disabled="true"> | ||||
|     <span class="title">{{ title }}</span> | ||||
|   </li> | ||||
| </template> | ||||
| <script> | ||||
| export default { | ||||
|   componentName: 'WootDropdownMenu', | ||||
|   props: { | ||||
|     title: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .dropdown-menu--header { | ||||
|   list-style: none; | ||||
|  | ||||
|   .title { | ||||
|     width: 100%; | ||||
|     display: block; | ||||
|     text-align: left; | ||||
|     white-space: nowrap; | ||||
|     padding: var(--space-small) var(--space-small); | ||||
|     margin-top: var(--space-smaller); | ||||
|     font-size: var(--font-size-mini); | ||||
|     color: var(--s-600); | ||||
|     font-weight: var(--font-weight-medium); | ||||
|     border-radius: var(--border-radius-normal); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -35,8 +35,12 @@ export default { | ||||
|     .button { | ||||
|       width: 100%; | ||||
|       text-align: left; | ||||
|       color: var(--s-700); | ||||
|       white-space: nowrap; | ||||
|       display: inline-flex; | ||||
|       padding: var(--space-small); | ||||
|       padding-top: var(--space-small); | ||||
|       padding-bottom: var(--space-small); | ||||
|       border-radius: var(--border-radius-normal); | ||||
|  | ||||
|       &:hover { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user