fix: Update route permissions in the new primary menu (#3499)

* fix: Display rolewise primary sidebar

* Fix issues with roles

* Fix active style

* Fix accessible menu

* Fix key missing

* Changes menu icon size

Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
Pranav Raj S
2021-12-01 21:32:43 -08:00
committed by GitHub
parent b826319776
commit 8b4134c790
23 changed files with 282 additions and 349 deletions

View 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>

View File

@@ -10,7 +10,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import PrimaryNavItem from 'dashboard/modules/sidebar/components/PrimaryNavItem';
import PrimaryNavItem from './PrimaryNavItem';
export default {
components: { PrimaryNavItem },
@@ -21,7 +21,7 @@ export default {
}),
unreadCount() {
if (!this.notificationMetadata.unreadCount) {
return '0';
return '';
}
return this.notificationMetadata.unreadCount < 100

View File

@@ -25,7 +25,7 @@
variant="clear"
color-scheme="secondary"
size="small"
icon="ion-help-buoy"
icon="chat-help"
@click="$emit('show-support-chat-window')"
>
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}

View File

@@ -0,0 +1,112 @@
<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="menuItem.key === activeMenuItem"
/>
</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 './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './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: () => [],
},
activeMenuItem: {
type: String,
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,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>

View File

@@ -0,0 +1,167 @@
<template>
<div v-if="menuConfig.menuItems.length" class="main-nav secondary-menu">
<transition-group name="menu-list" tag="ul" class="menu vertical">
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
:key="menuItem.toState"
:menu-item="menuItem"
/>
<secondary-nav-item
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key"
:menu-item="menuItem"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
</template>
<script>
import { frontendURL } from '../../../helper/URLHelper';
import SecondaryNavItem from './SecondaryNavItem.vue';
export default {
components: {
SecondaryNavItem,
},
props: {
accountId: {
type: Number,
default: 0,
},
labels: {
type: Array,
default: () => [],
},
inboxes: {
type: Array,
default: () => [],
},
teams: {
type: Array,
default: () => [],
},
menuConfig: {
type: Object,
default: () => {},
},
currentRole: {
type: String,
default: '',
},
},
computed: {
accessibleMenuItems() {
if (!this.currentRole) {
return [];
}
return this.menuConfig.menuItems.filter(
menuItem =>
window.roleWiseRoutes[this.currentRole].indexOf(
menuItem.toStateName
) > -1
);
},
inboxSection() {
return {
icon: 'folder',
label: 'INBOXES',
hasSubMenu: true,
newLink: true,
newLinkTag: 'NEW_INBOX',
key: 'inbox',
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',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
children: this.labels.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',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
children: this.labels.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,
newLinkTag: 'NEW_TEAM',
key: 'team',
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}`),
})),
};
},
additionalSecondaryMenuItems() {
let conversationMenuItems = [this.inboxSection, this.labelSection];
if (this.teams.length) {
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
}
return {
conversations: conversationMenuItems,
contacts: [this.contactLabelSection],
};
},
},
methods: {
showAddLabelPopup() {
this.$emit('add-label');
},
},
};
</script>

View File

@@ -0,0 +1,145 @@
<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="12" />
</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-normal);
$label-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;
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
background: var(--s-100);
}
.badge--icon {
align-items: center;
height: $badge-size;
justify-content: center;
min-width: $badge-size;
}
.badge--label {
height: $label-badge-size;
min-width: $label-badge-size;
margin-left: var(--space-smaller);
}
.badge.secondary {
min-width: unset;
background: var(--s-75);
color: var(--s-600);
font-weight: var(--font-weight-bold);
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<li class="sidebar-item">
<span v-if="hasSubMenu" class="secondary-menu--title fs-small">
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<router-link
v-else
class="secondary-menu--title secondary-menu--link fs-small"
:class="computedClass"
:to="menuItem.toState"
>
<fluent-icon
:icon="menuItem.icon"
class="secondary-menu--icon"
size="14"
/>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</router-link>
<ul v-if="hasSubMenu" class="nested vertical menu">
<secondary-child-nav-item
v-for="child in menuItem.children"
:key="child.id"
:to="child.toState"
:label="child.label"
:label-color="child.color"
:should-truncate="child.truncateLabel"
:icon="computedInboxClass(child)"
/>
<router-link
v-if="showItem(menuItem)"
v-slot="{ href, isActive, navigate }"
:to="menuItem.toState"
custom
>
<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>
</li>
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import { getInboxClassByType } from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem';
export default {
components: { SecondaryChildNavItem },
mixins: [adminMixin],
props: {
menuItem: {
type: Object,
default: () => ({}),
},
},
computed: {
...mapGetters({ activeInbox: 'getSelectedInbox' }),
hasSubMenu() {
return !!this.menuItem.children;
},
computedClass() {
// If active Inbox is present
// donot highlight conversations
if (this.activeInbox) return ' ';
if (
this.$store.state.route.name === 'inbox_conversation' &&
this.menuItem.toStateName === 'home'
) {
return 'is-active';
}
return ' ';
},
},
methods: {
computedInboxClass(child) {
const { type, phoneNumber } = child;
if (!type) return '';
const classByType = getInboxClassByType(type, phoneNumber);
return classByType;
},
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');
}
}
},
showItem(item) {
return this.isAdmin && item.newLink !== undefined;
},
},
};
</script>
<style lang="scss" scoped>
.sidebar-item {
margin: var(--space-smaller) 0 0;
}
.secondary-menu--title {
color: var(--s-600);
display: flex;
font-weight: var(--font-weight-bold);
line-height: var(--space-two);
margin: var(--space-small) 0;
padding: 0 var(--space-small);
}
.secondary-menu--link {
display: flex;
align-items: center;
margin: 0;
padding: var(--space-small);
font-weight: var(--font-weight-medium);
border-radius: var(--border-radius-normal);
&:hover {
background: var(--s-25);
color: var(--s-600);
}
&:focus {
border-color: var(--w-300);
}
&.router-link-exact-active,
&.is-active {
background: var(--w-25);
color: var(--w-500);
border-color: var(--w-25);
}
}
.secondary-menu--icon {
margin-right: var(--space-smaller);
min-width: var(--space-normal);
}
.sub-menu-link {
color: var(--s-600);
}
.wrap {
display: flex;
align-items: center;
}
.label-color--display {
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>