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>
This commit is contained in:
Nithin David Thomas 2021-08-11 12:29:31 +05:30 committed by GitHub
parent e834b545ef
commit 030df7afe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 716 additions and 364 deletions

View file

@ -49,7 +49,12 @@ code {
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
// remove when grid gutters are fixed // remove when grid gutters are fixed
.columns.with-right-space { .columns.with-right-space {
padding-right: var(--space-normal); padding-right: var(--space-normal);
} }
.badge {
border-radius: var(--border-radius-normal);
}

View file

@ -219,9 +219,9 @@ $badge-background: $primary-color;
$badge-color: $white; $badge-color: $white;
$badge-color-alt: $black; $badge-color-alt: $black;
$badge-palette: $foundation-palette; $badge-palette: $foundation-palette;
$badge-padding: 0.3em; $badge-padding: var(--space-smaller);
$badge-minwidth: 2.1em; $badge-minwidth: 2.1em;
$badge-font-size: 0.6rem; $badge-font-size: var(--font-size-nano);
// 10. Breadcrumbs // 10. Breadcrumbs
// --------------- // ---------------
@ -400,7 +400,7 @@ $mediaobject-image-width-stacked: 100%;
$menu-margin: 0; $menu-margin: 0;
$menu-margin-nested: $space-medium; $menu-margin-nested: $space-medium;
$menu-item-padding: $space-one; $menu-item-padding: $space-slab;
$menu-item-color-active: $white; $menu-item-color-active: $white;
$menu-item-background-active: $color-background; $menu-item-background-active: $color-background;
$menu-icon-spacing: 0.25rem; $menu-icon-spacing: 0.25rem;

View file

@ -44,11 +44,14 @@ $woot-logo-padding: $space-large $space-two;
$color-woot: #1f93ff; $color-woot: #1f93ff;
$color-gray: #6e6f73; $color-gray: #6e6f73;
$color-light-gray: #999a9b; $color-light-gray: #999a9b;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5; $color-border: var(--s-75);
$color-border-dark: #cad0d4; $color-border-light: var(--s-50);
$color-background: #f4f6fb; $color-border-dark: var(--s-100);
$color-background-light: #f9fafc;
$color-background: var(--s-50);
$color-background-light: var(--s-25);
$color-white: #fff; $color-white: #fff;
$color-body: #3c4858; $color-body: #3c4858;
$color-heading: #1f2d3d; $color-heading: #1f2d3d;

View file

@ -8,7 +8,7 @@
@include background-white; @include background-white;
@include flex; @include flex;
@include flex-align($x: justify, $y: middle); @include flex-align($x: justify, $y: middle);
@include border-normal-bottom; border-bottom: 1px solid var(--s-50);
height: $header-height; height: $header-height;
min-height: $header-height; min-height: $header-height;

View file

@ -6,12 +6,6 @@
} }
.sidebar { .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; z-index: 1024 - 1;
//logo //logo
@ -22,26 +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 {
&:hover {
color: $color-woot;
}
}
}
.active a .wrap {
color: $color-woot;
}
}
.nested { .nested {
a { a {
font-size: $font-size-small; font-size: $font-size-small;
@ -83,34 +57,6 @@
} }
} }
.main-nav {
@include flex-weight(1);
@include scroll-on-hover;
padding: 0 $space-medium - $space-one;
a {
&::before {
margin-right: $space-slab;
}
}
.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;
}
.hamburger--menu { .hamburger--menu {
cursor: pointer; cursor: pointer;
display: none; display: none;

View file

@ -1,51 +1,23 @@
<template> <template>
<div class="status"> <woot-dropdown-menu>
<div class="status-view"> <woot-dropdown-item
<availability-status-badge :status="currentUserAvailabilityStatus" /> v-for="status in availabilityStatuses"
<div class="status-view--title"> :key="status.value"
{{ availabilityDisplayLabel }} class="status-items"
</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)"
>
<availability-status-badge :status="status.value" />
{{ status.label }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
</transition>
<woot-button <woot-button
variant="clear" variant="clear"
size="small"
color-scheme="secondary" color-scheme="secondary"
class-names="status-change--change-button link" class-names="status-change--dropdown-button"
@click="openStatusMenu" :is-disabled="status.disabled"
@click="changeAvailabilityStatus(status.value)"
> >
{{ $t('SIDEBAR_ITEMS.CHANGE_AVAILABILITY_STATUS') }} <availability-status-badge :status="status.value" />
{{ status.label }}
</woot-button> </woot-button>
</div> </woot-dropdown-item>
</div> </woot-dropdown-menu>
</template> </template>
<script> <script>

View file

@ -1,18 +1,15 @@
<template> <template>
<aside class="sidebar animated shrink columns"> <aside class="woot-sidebar" :class="{ 'only-primary': !showSecondaryMenu }">
<div class="logo"> <primary-sidebar
<router-link :to="dashboardPath" replace> :logo-source="globalConfig.logo"
<img :src="globalConfig.logo" :alt="globalConfig.installationName" /> :installation-name="globalConfig.installationName"
</router-link> :account-id="accountId"
</div> :menu-items="primaryMenuItems"
@toggle-accounts="toggleAccountModal"
/>
<div class="main-nav"> <div v-if="showSecondaryMenu" class="main-nav secondary-menu">
<transition-group name="menu-list" tag="ul" class="menu vertical"> <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 <sidebar-item
v-if="shouldShowTeams" v-if="shouldShowTeams"
:key="teamSection.toState" :key="teamSection.toState"
@ -35,25 +32,19 @@
:menu-item="contactLabelSection" :menu-item="contactLabelSection"
@add-label="showAddLabelPopup" @add-label="showAddLabelPopup"
/> />
<sidebar-item
v-if="shouldShowSettingsSideMenu"
:key="settingsSubMenu.key"
:menu-item="settingsSubMenu"
/>
<sidebar-item
v-if="shouldShowNotificationsSideMenu"
:key="notificationsSubMenu.key"
:menu-item="notificationsSubMenu"
/>
</transition-group> </transition-group>
</div> </div>
<div class="bottom-nav">
<availability-status />
</div>
<div class="bottom-nav app-context-menu" @click="toggleOptions">
<agent-details @show-options="toggleOptions" />
<notification-bell />
<span class="current-user--options icon ion-android-more-vertical" />
<options-menu
:show="showOptionsMenu"
@toggle-accounts="toggleAccountModal"
@show-support-chat-window="toggleSupportChatWindow"
@close="toggleOptions"
/>
</div>
<account-selector <account-selector
:show-account-modal="showAccountModal" :show-account-modal="showAccountModal"
@close-account-modal="toggleAccountModal" @close-account-modal="toggleAccountModal"
@ -76,27 +67,22 @@ import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
import SidebarItem from './SidebarItem'; import SidebarItem from './SidebarItem';
import AvailabilityStatus from './AvailabilityStatus';
import { frontendURL } from '../../helper/URLHelper'; import { frontendURL } from '../../helper/URLHelper';
import { getSidebarItems } from '../../i18n/default-sidebar'; import { getSidebarItems } from '../../i18n/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin'; 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 AccountSelector from './sidebarComponents/AccountSelector.vue';
import AddAccountModal from './sidebarComponents/AddAccountModal.vue'; import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel'; import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
import PrimarySidebar from 'dashboard/modules/sidebar/components/Primary';
export default { export default {
components: { components: {
AgentDetails,
SidebarItem, SidebarItem,
AvailabilityStatus,
NotificationBell,
OptionsMenu,
AccountSelector, AccountSelector,
AddAccountModal, AddAccountModal,
AddLabelModal, AddLabelModal,
PrimarySidebar,
}, },
mixins: [adminMixin, alertMixin], mixins: [adminMixin, alertMixin],
data() { data() {
@ -122,6 +108,13 @@ export default {
sidemenuItems() { sidemenuItems() {
return getSidebarItems(this.accountId); return getSidebarItems(this.accountId);
}, },
primaryMenuItems() {
const menuItems = Object.values(
getSidebarItems(this.accountId).common.menuItems
);
return menuItems;
},
accessibleMenuItems() { accessibleMenuItems() {
// get all keys in menuGroup // get all keys in menuGroup
const groupKey = Object.keys(this.sidemenuItems); const groupKey = Object.keys(this.sidemenuItems);
@ -148,6 +141,17 @@ export default {
showShowContactSideMenu() { showShowContactSideMenu() {
return this.sidemenuItems.contacts.routes.includes(this.currentRoute); return this.sidemenuItems.contacts.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
);
},
shouldShowTeams() { shouldShowTeams() {
return this.shouldShowSidebarItem && this.teams.length; return this.shouldShowSidebarItem && this.teams.length;
}, },
@ -157,6 +161,7 @@ export default {
label: 'INBOXES', label: 'INBOXES',
hasSubMenu: true, hasSubMenu: true,
newLink: true, newLink: true,
newLinkTag: 'NEW_INBOX',
key: 'inbox', key: 'inbox',
cssClass: 'menu-title align-justify', cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`), toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`),
@ -177,6 +182,7 @@ export default {
label: 'LABELS', label: 'LABELS',
hasSubMenu: true, hasSubMenu: true,
newLink: true, newLink: true,
newLinkTag: 'NEW_LABEL',
key: 'label', key: 'label',
cssClass: 'menu-title align-justify', cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`), toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
@ -201,6 +207,7 @@ export default {
hasSubMenu: true, hasSubMenu: true,
key: 'label', key: 'label',
newLink: false, newLink: false,
newLinkTag: 'NEW_LABEL',
cssClass: 'menu-title align-justify', cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`), toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list', toStateName: 'labels_list',
@ -223,6 +230,7 @@ export default {
label: 'TEAMS', label: 'TEAMS',
hasSubMenu: true, hasSubMenu: true,
newLink: true, newLink: true,
newLinkTag: 'NEW_TEAM',
key: 'team', key: 'team',
cssClass: 'menu-title align-justify teams-sidebar-menu', cssClass: 'menu-title align-justify teams-sidebar-menu',
toState: frontendURL(`accounts/${this.accountId}/settings/teams`), toState: frontendURL(`accounts/${this.accountId}/settings/teams`),
@ -236,9 +244,49 @@ export default {
})), })),
}; };
}, },
settingsSubMenu() {
const menuItems = Object.values(this.sidemenuItems.settings.menuItems);
return {
icon: 'ion-settings',
label: 'SETTINGS',
hasSubMenu: true,
key: 'settings',
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings`),
children: menuItems.map(item => ({
...item,
label: this.$t(`SIDEBAR.${item.label}`),
})),
};
},
reportsSubMenu() {
return {
icon: 'ion-ion-arrow-graph-up-right',
cssClass: 'menu-title align-justify',
label: 'REPORTS',
hasSubMenu: false,
key: 'reports',
children: [],
};
},
notificationsSubMenu() {
return {
icon: 'ion-ios-bell',
label: 'NOTIFICATIONS',
hasSubMenu: false,
cssClass: 'menu-title align-justify',
key: 'notifications',
children: [],
};
},
dashboardPath() { dashboardPath() {
return frontendURL(`accounts/${this.accountId}/dashboard`); return frontendURL(`accounts/${this.accountId}/dashboard`);
}, },
showSecondaryMenu() {
if (this.shouldShowReportsSideMenu) return false;
if (this.shouldShowNotificationsSideMenu) return false;
return true;
},
}, },
watch: { watch: {
currentUser(newUserInfo, oldUserInfo) { currentUser(newUserInfo, oldUserInfo) {
@ -280,9 +328,7 @@ export default {
) > -1 ) > -1
); );
}, },
toggleOptions() {
this.showOptionsMenu = !this.showOptionsMenu;
},
toggleAccountModal() { toggleAccountModal() {
this.showAccountModal = !this.showAccountModal; this.showAccountModal = !this.showAccountModal;
}, },
@ -303,6 +349,26 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped>
.woot-sidebar {
display: flex;
&.only-primary {
width: auto;
}
}
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100vh;
width: 216px;
flex-shrink: 0;
overflow: auto;
padding: var(--space-small);
}
</style>
<style lang="scss"> <style lang="scss">
@import '~dashboard/assets/scss/variables'; @import '~dashboard/assets/scss/variables';
@ -367,7 +433,7 @@ export default {
margin-top: auto; margin-top: auto;
} }
.teams-sidebar-menu + .nested.vertical.menu { .secondary-menu .nested.vertical.menu {
padding-left: calc(var(--space-medium) - var(--space-one)); margin-left: var(--space-small);
} }
</style> </style>

View file

@ -1,10 +1,5 @@
<template> <template>
<router-link <li :class="computedClass" class="sidebar-item">
:to="menuItem.toState"
tag="li"
active-class="active"
:class="computedClass"
>
<a <a
class="sub-menu-title" class="sub-menu-title"
:class="getMenuItemClass" :class="getMenuItemClass"
@ -12,63 +7,57 @@
aria-haspopup="true" aria-haspopup="true"
:title="menuItem.toolTip" :title="menuItem.toolTip"
> >
<div class="wrap"> {{ $t(`SIDEBAR.${menuItem.label}`) }}
<i :class="menuItem.icon" />
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</div>
<span
v-if="showItem(menuItem)"
class="child-icon ion-android-add-circle"
@click.prevent="newLinkClick(menuItem)"
/>
</a> </a>
<ul v-if="menuItem.hasSubMenu" class="nested vertical menu"> <ul v-if="menuItem.hasSubMenu" class="nested vertical menu">
<router-link <secondary-nav-item
v-for="child in menuItem.children" v-for="child in menuItem.children"
:key="child.id" :key="child.id"
active-class="active flex-container"
tag="li"
:to="child.toState" :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)"> <li>
<div class="wrap"> <a
<i :href="href"
v-if="computedInboxClass(child)" class="button small clear menu-item--new secondary"
class="inbox-icon" :class="{ 'is-active': isActive }"
:class="computedInboxClass(child)" @click="e => newLinkClick(e, navigate)"
/> >
<span <i class="icon ion-plus-round" />
v-if="child.color" <span class="button__content">
class="label-color--display" {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
:style="{ backgroundColor: child.color }" </span>
/> </a>
<div </li>
:title="computedChildTitle(child)"
:class="computedChildClass(child)"
>
{{ child.label }}
</div>
</div>
</a>
</router-link> </router-link>
</ul> </ul>
</router-link> </li>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import router from '../../routes';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
import { getInboxClassByType } from 'dashboard/helper/inbox'; import { getInboxClassByType } from 'dashboard/helper/inbox';
import SecondaryNavItem from 'dashboard/modules/sidebar/components/SecondaryNavItem';
export default { export default {
components: { SecondaryNavItem },
mixins: [adminMixin], mixins: [adminMixin],
props: { props: {
menuItem: { menuItem: {
type: Object, type: Object,
default() { default: () => ({}),
return {};
},
}, },
}, },
computed: { computed: {
@ -108,11 +97,11 @@ export default {
if (!child.truncateLabel) return false; if (!child.truncateLabel) return false;
return child.label; return child.label;
}, },
newLinkClick(item) { newLinkClick(e, navigate) {
if (item.newLinkRouteName) { if (this.menuItem.newLinkRouteName) {
router.push({ name: item.newLinkRouteName, params: { page: 'new' } }); navigate(e);
} else if (item.showModalForNewItem) { } else if (this.menuItem.showModalForNewItem) {
if (item.modalName === 'AddLabel') { if (this.menuItem.modalName === 'AddLabel') {
this.$emit('add-label'); this.$emit('add-label');
} }
} }
@ -124,11 +113,22 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~dashboard/assets/scss/variables'; .sidebar-item {
margin: var(--space-small) 0;
}
.sub-menu-title { .sub-menu-title {
display: flex; display: flex;
justify-content: space-between; 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 { .wrap {
@ -137,11 +137,11 @@ export default {
} }
.label-color--display { .label-color--display {
border-radius: $space-smaller; border-radius: var(--space-smaller);
height: $space-normal; height: var(--space-normal);
margin-right: $space-small; margin-right: var(--space-small);
min-width: $space-normal; min-width: var(--space-normal);
width: $space-normal; width: var(--space-normal);
} }
.inbox-icon { .inbox-icon {
@ -151,4 +151,12 @@ export default {
font-size: var(--font-size-medium); font-size: var(--font-size-medium);
} }
} }
.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);
}
</style> </style>

View file

@ -1,18 +1,12 @@
<template> <template>
<div class="current-user--row"> <woot-button variant="link" class="current-user" @click="handleClick">
<thumbnail <thumbnail
:src="currentUser.avatar_url" :src="currentUser.avatar_url"
:username="currentUserAvailableName" :username="currentUser.name"
:status="currentUser.availability_status"
size="32px"
/> />
<div class="current-user--data"> </woot-button>
<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>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@ -27,37 +21,20 @@ export default {
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
currentRole: 'getCurrentRole', currentRole: 'getCurrentRole',
}), }),
currentUserAvailableName() { },
return this.currentUser.name; methods: {
handleClick() {
this.$emit('toggle-menu');
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.current-user--row { .current-user {
align-items: center; align-items: center;
display: flex; display: flex;
} border-radius: 50%;
border: 2px solid var(--white);
.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;
}
} }
</style> </style>

View file

@ -1,11 +1,18 @@
<template> <template>
<span class="notifications icon ion-ios-bell" @click.stop="showNotification"> <div class="notifications-link">
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span> <primary-nav-item
</span> icon="ion-ios-bell"
:to="`/app/accounts/${accountId}/notifications`"
:count="unreadCount"
/>
</div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import PrimaryNavItem from 'dashboard/modules/sidebar/components/PrimaryNavItem';
export default { export default {
components: { PrimaryNavItem },
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
@ -13,40 +20,20 @@ export default {
}), }),
unreadCount() { unreadCount() {
if (!this.notificationMetadata.unreadCount) { if (!this.notificationMetadata.unreadCount) {
return 0; return '0';
} }
return this.notificationMetadata.unreadCount < 100 return this.notificationMetadata.unreadCount < 100
? this.notificationMetadata.unreadCount ? `${this.notificationMetadata.unreadCount}`
: '99+'; : '99+';
}, },
}, },
methods: { methods: {},
showNotification() {
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
},
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.notifications { .notifications-link {
font-size: var(--font-size-big); margin-bottom: var(--space-small);
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);
}
} }
</style> </style>

View file

@ -2,14 +2,18 @@
<transition name="menu-slide"> <transition name="menu-slide">
<div <div
v-if="show" v-if="show"
v-on-clickaway="() => $emit('close')" v-on-clickaway="onClickAway"
class="dropdown-pane dropdowm--top" class="dropdown-pane"
:class="{ 'dropdown-pane--open': show }"
> >
<availability-status />
<woot-dropdown-menu> <woot-dropdown-menu>
<woot-dropdown-item v-if="showChangeAccountOption"> <woot-dropdown-item v-if="showChangeAccountOption">
<woot-button <woot-button
variant="clear" variant="clear"
color-scheme="secondary"
size="small" size="small"
icon="ion-arrow-swap"
class=" change-accounts--button" class=" change-accounts--button"
@click="$emit('toggle-accounts')" @click="$emit('toggle-accounts')"
> >
@ -19,7 +23,9 @@
<woot-dropdown-item v-if="globalConfig.chatwootInboxToken"> <woot-dropdown-item v-if="globalConfig.chatwootInboxToken">
<woot-button <woot-button
variant="clear" variant="clear"
color-scheme="secondary"
size="small" size="small"
icon="ion-help-buoy"
class=" change-accounts--button" class=" change-accounts--button"
@click="$emit('show-support-chat-window')" @click="$emit('show-support-chat-window')"
> >
@ -29,15 +35,20 @@
<woot-dropdown-item> <woot-dropdown-item>
<router-link <router-link
:to="`/app/accounts/${accountId}/profile/settings`" :to="`/app/accounts/${accountId}/profile/settings`"
class="button clear small change-accounts--button" class="button clear small secondary change-accounts--button"
> >
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }} <i class="icon ion-person" />
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>
</router-link> </router-link>
</woot-dropdown-item> </woot-dropdown-item>
<woot-dropdown-item> <woot-dropdown-item>
<woot-button <woot-button
variant="clear" variant="clear"
color-scheme="secondary"
size="small" size="small"
icon="ion-log-out"
class=" change-accounts--button" class=" change-accounts--button"
@click="logout" @click="logout"
> >
@ -53,13 +64,15 @@
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Auth from '../../../api/auth'; import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus';
export default { export default {
components: { components: {
WootDropdownMenu, WootDropdownMenu,
WootDropdownItem, WootDropdownItem,
AvailabilityStatus,
}, },
mixins: [clickaway], mixins: [clickaway],
props: { props: {
@ -78,7 +91,9 @@ export default {
if (this.globalConfig.createNewAccountFromDashboard) { if (this.globalConfig.createNewAccountFromDashboard) {
return true; return true;
} }
return this.currentUser.accounts.length > 1;
const { accounts = [] } = this.currentUser;
return accounts.length > 1;
}, },
}, },
methods: { methods: {
@ -89,11 +104,15 @@ export default {
window.$chatwoot.reset(); window.$chatwoot.reset();
} }
}, },
onClickAway() {
if (this.show) this.$emit('close');
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-pane { .dropdown-pane {
right: 0; left: var(--space-slab);
bottom: var(--space-larger);
} }
</style> </style>

View file

@ -5,10 +5,12 @@ import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import WootButton from 'dashboard/components/ui/WootButton';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
localVue.use(VueI18n); localVue.use(VueI18n);
localVue.component('thumbnail', Thumbnail); localVue.component('thumbnail', Thumbnail);
localVue.component('woot-button', WootButton);
const i18nConfig = new VueI18n({ const i18nConfig = new VueI18n({
locale: 'en', locale: 'en',
@ -16,7 +18,11 @@ const i18nConfig = new VueI18n({
}); });
describe('agentDetails', () => { describe('agentDetails', () => {
const currentUser = { name: 'Neymar Junior', avatar_url: '' }; const currentUser = {
name: 'Neymar Junior',
avatar_url: '',
availability_status: 'online',
};
const currentRole = 'agent'; const currentRole = 'agent';
let store = null; let store = null;
let actions = null; let actions = null;
@ -47,14 +53,8 @@ describe('agentDetails', () => {
}); });
}); });
it('shows the agent name', () => { it(' the agent status', () => {
const agentTitle = agentDetails.find('.current-user--name'); expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
expect(agentTitle.text()).toBe('Neymar Junior');
});
it('shows the agent role', () => {
const agentTitle = agentDetails.find('.current-user--role');
expect(agentTitle.text()).toBe('Agent');
}); });
it('agent thumbnail exists', () => { it('agent thumbnail exists', () => {

View file

@ -50,8 +50,9 @@ describe('notificationBell', () => {
localVue, localVue,
i18n: i18nConfig, 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 () => { it('it should return unread count 99+ ', async () => {
@ -61,7 +62,7 @@ describe('notificationBell', () => {
localVue, localVue,
i18n: i18nConfig, i18n: i18nConfig,
}); });
const statusViewTitle = notificationBell.find('.unread-badge'); const statusViewTitle = notificationBell.find('primary-nav-item-stub');
expect(statusViewTitle.text()).toBe('99+'); expect(statusViewTitle.vm.count).toBe('99+');
}); });
}); });

View file

@ -17,7 +17,7 @@ const i18nConfig = new VueI18n({
}); });
describe('AvailabilityStatus', () => { describe('AvailabilityStatus', () => {
const currentUser = { availability_status: 'online' }; const currentUser = { availability_status: 'offline' };
let store = null; let store = null;
let actions = null; let actions = null;
let modules = null; let modules = null;
@ -50,34 +50,12 @@ 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 () => { it('dispatches an action when user changes status', async () => {
await availabilityStatus await availabilityStatus.find('button:first-child').trigger('click');
.find('.status-change--change-button')
.trigger('click');
await availabilityStatus
.find('.status-change li:last-child button')
.trigger('click');
expect(actions.updateAvailability).toBeCalledWith( expect(actions.updateAvailability).toBeCalledWith(
expect.any(Object), expect.any(Object),
{ availability: 'offline' }, { availability: 'online' },
undefined undefined
); );
}); });

View file

@ -44,6 +44,7 @@
@toggle-user-mention="toggleUserMention" @toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu" @toggle-canned-menu="toggleCannedMenu"
/> />
<h1>{{ message }}</h1>
</div> </div>
<div v-if="hasAttachments" class="attachment-preview-box"> <div v-if="hasAttachments" class="attachment-preview-box">
<attachment-preview <attachment-preview

View file

@ -1,5 +1,7 @@
import { frontendURL } from '../helper/URLHelper'; import { frontendURL } from '../helper/URLHelper';
// TODO - find hasSubMenu usage - July/2021
export const getSidebarItems = accountId => ({ export const getSidebarItems = accountId => ({
common: { common: {
routes: [ routes: [
@ -33,13 +35,6 @@ export const getSidebarItems = accountId => ({
toState: frontendURL(`accounts/${accountId}/contacts`), toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard', toStateName: 'contacts_dashboard',
}, },
notifications: {
icon: 'ion-ios-bell',
label: 'NOTIFICATIONS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/notifications`),
toStateName: 'notifications_dashboard',
},
report: { report: {
icon: 'ion-arrow-graph-up-right', icon: 'ion-arrow-graph-up-right',
label: 'REPORTS', label: 'REPORTS',
@ -105,6 +100,10 @@ export const getSidebarItems = accountId => ({
}, },
}, },
}, },
notifications: {
routes: ['notifications_index'],
menuItems: {},
},
settings: { settings: {
routes: [ routes: [
'agent_list', 'agent_list',
@ -134,13 +133,6 @@ export const getSidebarItems = accountId => ({
'settings_teams_edit_finish', 'settings_teams_edit_finish',
], ],
menuItems: { menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
agents: { agents: {
icon: 'ion-person-stalker', icon: 'ion-person-stalker',
label: 'AGENTS', label: 'AGENTS',

View file

@ -140,6 +140,9 @@
"TEAMS": "Teams", "TEAMS": "Teams",
"ALL_CONTACTS": "All Contacts", "ALL_CONTACTS": "All Contacts",
"TAGGED_WITH": "Tagged with", "TAGGED_WITH": "Tagged with",
"NEW_LABEL": "New label",
"NEW_TEAM": "New team",
"NEW_INBOX": "New inbox",
"REPORTS_OVERVIEW": "Overview", "REPORTS_OVERVIEW": "Overview",
"CSAT": "CSAT" "CSAT": "CSAT"
}, },

View file

@ -0,0 +1,44 @@
<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: 40px;
.logo {
padding: var(--space-normal);
img {
width: $logo-size;
height: $logo-size;
}
}
</style>

View file

@ -0,0 +1,106 @@
<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"
/>
</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"
@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();
},
},
};
</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>

View file

@ -0,0 +1,73 @@
<template>
<router-link v-slot="{ href, isActive, navigate }" :to="to" custom>
<a
:href="href"
class="button clear button--only-icon menu-item"
:class="{ 'is-active': isActive }"
@click="navigate"
>
<i class="icon" :class="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: '',
},
},
};
</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>

View file

@ -0,0 +1,34 @@
<template>
<div class="secondary--sidebar">
<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"
/>
<sidebar-item
v-if="showShowContactSideMenu"
:key="contactLabelSection.key"
:menu-item="contactLabelSection"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
</div>
</template>

View file

@ -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"
:class="{ 'is-active': isActive, 'text-truncate': shouldTruncate }"
@click="navigate"
>
<span v-if="icon" class="badge--icon">
<i class="icon inbox-icon" :class="icon" />
</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>

View file

@ -2,16 +2,17 @@
--white: #fff; --white: #fff;
--white-transparent: rgba(255, 255, 255, 0.9); --white-transparent: rgba(255, 255, 255, 0.9);
--w-50: #E3F2FF; --w-25: #F5FAFF;
--w-100: #BBDDFF; --w-50: #EBF5FF;
--w-200: #8FC9FF; --w-100: #C2E1FF;
--w-300: #61B3FF; --w-200: #99CEFF;
--w-400: #3FA3FF; --w-300: ##70BAFF;
--w-400: #47A6FF;
--w-500: #1F93FF; --w-500: #1F93FF;
--w-600: #2284F0; --w-600: #1976CC;
--w-700: #2272DC; --w-700: #135899;
--w-800: #2161CA; --w-800: #0C3B66;
--w-900: #1F41AB; --w-900: #061D33;
--g-50: #E6F8E6; --g-50: #E6F8E6;
--g-100: #C4EEC2; --g-100: #C4EEC2;
@ -35,16 +36,18 @@
--y-800: #FDAD2A; --y-800: #FDAD2A;
--y-900: #F9841B; --y-900: #F9841B;
--s-50: #E7EEFB; --s-25: #F8FAFC;
--s-100: #C8D6E6; --s-50: #F1F5F8;
--s-200: #ABBACE; --s-75: #EBF0F5;
--s-300: #8C9EB6; --s-100: #E4EBF1;
--s-400: #7489A4; --s-200: #C9D7E3;
--s-500: #5D7592; --s-300: #AEC3D5;
--s-600: #506781; --s-400: #93AFC8;
--s-700: #40546B; --s-500: #779BBB;
--s-800: #314155; --s-600: #446888;
--s-900: #1F2D3D; --s-700: #37546D;
--s-800: #293F51;
--s-900: #1B2836;
--b-50: #F8F9FE; --b-50: #F8F9FE;
--b-100: #F2F3F7; --b-100: #F2F3F7;
@ -85,12 +88,12 @@
--color-heading: #1f2d3d; --color-heading: #1f2d3d;
--color-body: #3c4858; --color-body: #3c4858;
--color-border: #e0e6ed; --color-border: var(--s-75);
--color-border-light: #f0f4f5; --color-border-light: var(--s-50);
--color-border-dark: #cad0d4; --color-border-dark: var(--s-100);
--color-background: #f4f6fb; --color-background: var(--s-50);
--color-background-light: #f9fafc; --color-background-light: var(--s-25);
// Social and inboxes brand colors // Social and inboxes brand colors
--color-facebook-brand: #3b5998; --color-facebook-brand: #3b5998;

View file

@ -24,7 +24,6 @@ export const actions = {
refreshActionCableConnector(pubsubToken); refreshActionCableConnector(pubsubToken);
dispatch('conversationAttributes/getAttributes', {}, { root: true }); dispatch('conversationAttributes/getAttributes', {}, { root: true });
} catch (error) { } catch (error) {
console.log(error);
// Ignore error // Ignore error
} finally { } finally {
commit('setConversationUIFlag', { isCreating: false }); commit('setConversationUIFlag', { isCreating: false });

View file

@ -59,7 +59,7 @@
"vue-i18n": "8.24.3", "vue-i18n": "8.24.3",
"vue-loader": "15.9.6", "vue-loader": "15.9.6",
"vue-multiselect": "~2.1.6", "vue-multiselect": "~2.1.6",
"vue-router": "~2.2.0", "vue-router": "3.5.2",
"vue-template-compiler": "2.6.12", "vue-template-compiler": "2.6.12",
"vue-upload-component": "2.8.22", "vue-upload-component": "2.8.22",
"vue2-datepicker": "^3.9.1", "vue2-datepicker": "^3.9.1",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 938 B

View file

@ -15045,10 +15045,10 @@ vue-resize@^1.0.1:
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
vue-router@~2.2.0: vue-router@3.5.2:
version "2.2.1" version "3.5.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.2.1.tgz#b027f9fac2cf13462725e843d6dc631b6aa077f6" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.2.tgz#5f55e3f251970e36c3e8d88a7cd2d67a350ade5c"
integrity sha1-sCf5+sLPE0YnJehD1txjG2qgd/Y= integrity sha512-807gn82hTnjCYGrnF3eNmIw/dk7/GE4B5h69BlyCK9KHASwSloD1Sjcn06zg9fVG4fYH2DrsNBZkpLtb25WtaQ==
vue-style-loader@^4.1.0: vue-style-loader@^4.1.0:
version "4.1.3" version "4.1.3"