mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
Feature: Ability to switch between multiple accounts (#881)
* Feature: Ability to switch between multiple accounts * Fix rubocop * Fix assigned inboxes * fix auth json * Add account switcher in UI * fix ordering on administrate * Add switch accounts to sidebar * add account id * Fix schema.rb timestamp * Revert "add account id" This reverts commit 27570f50ef584cb9a5f69454f43f630b318c8807. * Add a check for account Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
@@ -72,7 +72,7 @@
|
||||
@include border-light;
|
||||
display: block;
|
||||
left: 18%;
|
||||
top: -110%;
|
||||
top: -110px;
|
||||
visibility: visible;
|
||||
width: 80%;
|
||||
z-index: 999;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
:key="item.toState"
|
||||
:menu-item="item"
|
||||
/>
|
||||
|
||||
<sidebar-item
|
||||
v-if="shouldShowInboxes"
|
||||
:key="inboxSection.toState"
|
||||
@@ -30,7 +29,7 @@
|
||||
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
|
||||
:button-route="{ name: 'billing' }"
|
||||
:type="statusBarClass"
|
||||
:show-button="isAdmin()"
|
||||
:show-button="isAdmin"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
@@ -42,6 +41,14 @@
|
||||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li v-if="currentUser.accounts.length > 1">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="changeAccount"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
@@ -62,13 +69,35 @@
|
||||
{{ currentUser.name }}
|
||||
</h3>
|
||||
<h5 class="current-user--role">
|
||||
{{ currentUser.role }}
|
||||
{{ currentRole }}
|
||||
</h5>
|
||||
</div>
|
||||
<span class="current-user--options icon ion-android-more-vertical">
|
||||
</span>
|
||||
<span class="current-user--options icon ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show="showAccountModal"
|
||||
:on-close="onClose"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:key="account.id"
|
||||
class="account-selector"
|
||||
>
|
||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
||||
<label :for="account.name" class="account--details">
|
||||
<div class="account--name">{{ account.name }}</div>
|
||||
<div class="account--role">{{ account.role }}</div>
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -82,7 +111,7 @@ import SidebarItem from './SidebarItem';
|
||||
import WootStatusBar from '../widgets/StatusBar';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import Thumbnail from '../widgets/Thumbnail';
|
||||
import sidemenuItems from '../../i18n/default-sidebar';
|
||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -100,6 +129,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -109,15 +139,20 @@ export default {
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
subscriptionData: 'getSubscription',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
}),
|
||||
sidemenuItems() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
// get all keys in menuGroup
|
||||
const groupKey = Object.keys(sidemenuItems);
|
||||
const groupKey = Object.keys(this.sidemenuItems);
|
||||
|
||||
let menuItems = [];
|
||||
// Iterate over menuGroup to find the correct group
|
||||
for (let i = 0; i < groupKey.length; i += 1) {
|
||||
const groupItem = sidemenuItems[groupKey[i]];
|
||||
const groupItem = this.sidemenuItems[groupKey[i]];
|
||||
// Check if current route is included
|
||||
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
|
||||
if (isRouteIncluded) {
|
||||
@@ -135,7 +170,7 @@ export default {
|
||||
return this.$store.state.route.name;
|
||||
},
|
||||
shouldShowInboxes() {
|
||||
return sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
},
|
||||
inboxSection() {
|
||||
return {
|
||||
@@ -177,9 +212,6 @@ export default {
|
||||
trialMessage() {
|
||||
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
|
||||
},
|
||||
accountId() {
|
||||
return this.currentUser.account_id;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('inboxes/get');
|
||||
@@ -191,13 +223,14 @@ export default {
|
||||
);
|
||||
},
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
const { role } = this.currentUser;
|
||||
if (!role) {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
}
|
||||
return menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
|
||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||
menuItem.toStateName
|
||||
) > -1
|
||||
);
|
||||
},
|
||||
logout() {
|
||||
@@ -206,6 +239,80 @@ export default {
|
||||
showOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
changeAccount() {
|
||||
this.showAccountModal = true;
|
||||
},
|
||||
onClose() {
|
||||
this.showAccountModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.account-selector--modal {
|
||||
.modal-container {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
padding-bottom: $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
.change-accounts--button.button {
|
||||
font-weight: $font-weight-normal;
|
||||
font-size: $font-size-small;
|
||||
padding: $space-small $space-one;
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
li {
|
||||
a {
|
||||
padding: $space-small $space-one !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-selector {
|
||||
cursor: pointer;
|
||||
padding: $space-small $space-large;
|
||||
|
||||
.ion-ios-checkmark {
|
||||
font-size: $font-size-big;
|
||||
|
||||
& + .account--details {
|
||||
padding-left: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.account--details {
|
||||
padding-left: $space-large + $space-smaller;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-large;
|
||||
}
|
||||
|
||||
a {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
.account--name {
|
||||
cursor: pointer;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.account--role {
|
||||
cursor: pointer;
|
||||
font-size: $font-size-mini;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import auth from '../../api/auth';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
|
||||
const INBOX_TYPES = {
|
||||
WEB: 'Channel::WebWidget',
|
||||
@@ -78,6 +78,7 @@ const getInboxClassByType = type => {
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -119,7 +120,7 @@ export default {
|
||||
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
|
||||
},
|
||||
showItem(item) {
|
||||
return auth.isAdmin() && item.newLink !== undefined;
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- No inboxes attached -->
|
||||
<div v-if="!inboxesList.length">
|
||||
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
|
||||
<span v-if="isAdmin()">
|
||||
<span v-if="isAdmin">
|
||||
{{ $t('CONVERSATION.NO_INBOX_1') }}
|
||||
<br />
|
||||
<router-link :to="newInboxURL">
|
||||
@@ -17,7 +17,7 @@
|
||||
</router-link>
|
||||
{{ $t('CONVERSATION.NO_INBOX_2') }}
|
||||
</span>
|
||||
<span v-if="!isAdmin()">
|
||||
<span v-if="!isAdmin">
|
||||
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
};
|
||||
}
|
||||
|
||||
isAValidEvent = data => {
|
||||
return this.app.$store.getters.getCurrentAccountId === data.account_id;
|
||||
};
|
||||
|
||||
onMessageUpdated = data => {
|
||||
this.app.$store.dispatch('updateMessage', data);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { frontendURL } from '../helper/URLHelper';
|
||||
import auth from '../api/auth';
|
||||
|
||||
const user = auth.getCurrentUser() || {};
|
||||
const accountId = user.account_id;
|
||||
|
||||
export default {
|
||||
export const getSidebarItems = accountId => ({
|
||||
common: {
|
||||
routes: [
|
||||
'home',
|
||||
@@ -106,13 +102,13 @@ export default {
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
general_settings: {
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings',
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
}
|
||||
},
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_ACCOUNTS": "Switch Account",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile Settings",
|
||||
"LOGOUT": "Logout"
|
||||
},
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
}
|
||||
},
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_ACCOUNTS": "Changer de compte",
|
||||
"SELECTOR_SUBTITLE": "Sélectionnez un compte dans la liste suivante",
|
||||
"PROFILE_SETTINGS": "Paramètres de profil",
|
||||
"LOGOUT": "Se déconnecter"
|
||||
},
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
}
|
||||
},
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_ACCOUNTS": "Verwissel van profiel",
|
||||
"SELECTOR_SUBTITLE": "Selecteer een account in de volgende lijst",
|
||||
"PROFILE_SETTINGS": "Profiel instellingen",
|
||||
"LOGOUT": "Afmelden"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import Auth from '../api/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUserRole: 'getCurrentRole',
|
||||
}),
|
||||
isAdmin() {
|
||||
return Auth.isAdmin();
|
||||
return this.currentUserRole === 'administrator';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,11 +15,6 @@ export default {
|
||||
components: {
|
||||
Sidebar,
|
||||
},
|
||||
props: {
|
||||
mainViewComponent: String,
|
||||
sidebarMenu: String,
|
||||
page: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSidebarOpen: false,
|
||||
@@ -50,6 +45,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setCurrentAccountId', this.$route.params.accountId);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
bus.$on('sidemenu_icon_click', () => {
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BackButton from '../../../components/widgets/BackButton';
|
||||
import adminMixin from '../../../mixins/isAdmin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
},
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
headerTitle: {
|
||||
default: '',
|
||||
@@ -51,10 +53,6 @@ export default {
|
||||
iconClass() {
|
||||
return `icon ${this.icon} header--icon`;
|
||||
},
|
||||
isAdmin() {
|
||||
const { role } = this.currentUser;
|
||||
return role === 'administrator';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,6 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/general'),
|
||||
name: 'general_settings',
|
||||
roles: ['administrator'],
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p v-if="!inboxesList.length" class="no-items-error-message">
|
||||
{{ $t('INBOX_MGMT.LIST.404') }}
|
||||
<router-link
|
||||
v-if="isAdmin()"
|
||||
v-if="isAdmin"
|
||||
:to="addAccountScoping('settings/inboxes/new')"
|
||||
>
|
||||
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
|
||||
@@ -54,7 +54,7 @@
|
||||
:to="addAccountScoping(`settings/inboxes/${item.id}`)"
|
||||
>
|
||||
<woot-submit-button
|
||||
v-if="isAdmin()"
|
||||
v-if="isAdmin"
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS')"
|
||||
icon-class="ion-gear-b"
|
||||
button-class="link hollow grey-btn"
|
||||
@@ -62,7 +62,7 @@
|
||||
</router-link>
|
||||
|
||||
<woot-submit-button
|
||||
v-if="isAdmin()"
|
||||
v-if="isAdmin"
|
||||
:button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
|
||||
:loading="loading[item.id]"
|
||||
icon-class="ion-close-circled"
|
||||
@@ -111,7 +111,6 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import Settings from './Settings';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import auth from '../../../../api/auth';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
@@ -149,9 +148,6 @@ export default {
|
||||
this.selectedInbox.name
|
||||
} ?`;
|
||||
},
|
||||
accountId() {
|
||||
return auth.getCurrentUser().account_id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openSettings(inbox) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
path: 'list',
|
||||
name: 'settings_inbox_list',
|
||||
component: InboxHome,
|
||||
roles: ['administrator', 'agent'],
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
|
||||
@@ -52,10 +52,8 @@ export default {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
accountId() {
|
||||
return this.currentUser.account_id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
|
||||
@@ -164,7 +164,6 @@ export default {
|
||||
registration.pushManager
|
||||
.getSubscription()
|
||||
.then(subscription => {
|
||||
console.log(subscription);
|
||||
if (!subscription) {
|
||||
this.hasEnabledPushPermissions = false;
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'general_settings_index',
|
||||
name: 'profile_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import agent from './agents/agent.routes';
|
||||
import Auth from '../../../api/auth';
|
||||
import billing from './billing/billing.routes';
|
||||
import canned from './canned/canned.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
@@ -8,6 +7,7 @@ import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import account from './account/account.routes';
|
||||
import store from '../../../store';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
name: 'settings_home',
|
||||
roles: ['administrator', 'agent'],
|
||||
redirect: () => {
|
||||
if (Auth.isAdmin()) {
|
||||
if (store.getters.getCurrentRole === 'administrator') {
|
||||
return frontendURL('accounts/:accountId/settings/agents');
|
||||
}
|
||||
return frontendURL('accounts/:accountId/settings/canned-response');
|
||||
|
||||
@@ -22,6 +22,7 @@ const state = {
|
||||
expiry: null,
|
||||
},
|
||||
},
|
||||
currentAccountId: null,
|
||||
};
|
||||
|
||||
// getters
|
||||
@@ -34,6 +35,18 @@ export const getters = {
|
||||
return _state.currentUser.id;
|
||||
},
|
||||
|
||||
getCurrentAccountId(_state) {
|
||||
return _state.currentAccountId;
|
||||
},
|
||||
|
||||
getCurrentRole(_state) {
|
||||
const { accounts = [] } = _state.currentUser;
|
||||
const [currentAccount = {}] = accounts.filter(
|
||||
account => account.id === _state.currentAccountId
|
||||
);
|
||||
return currentAccount.role;
|
||||
},
|
||||
|
||||
getCurrentUser(_state) {
|
||||
return _state.currentUser;
|
||||
},
|
||||
@@ -103,6 +116,10 @@ export const actions = {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentAccountId({ commit }, accountId) {
|
||||
commit(types.default.SET_CURRENT_ACCOUNT_ID, accountId);
|
||||
},
|
||||
};
|
||||
|
||||
// mutations
|
||||
@@ -118,6 +135,9 @@ const mutations = {
|
||||
|
||||
Vue.set(_state, 'currentUser', currentUser);
|
||||
},
|
||||
[types.default.SET_CURRENT_ACCOUNT_ID](_state, accountId) {
|
||||
Vue.set(_state, 'currentAccountId', Number(accountId));
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -2,6 +2,8 @@ export default {
|
||||
AUTHENTICATE: 'AUTHENTICATE',
|
||||
CLEAR_USER: 'LOGOUT',
|
||||
SET_CURRENT_USER: 'SET_CURRENT_USER',
|
||||
SET_CURRENT_ACCOUNT_ID: 'SET_CURRENT_ACCOUNT_ID',
|
||||
|
||||
// Chat List
|
||||
RECEIVE_CHAT_LIST: 'RECEIVE_CHAT_LIST',
|
||||
SET_ALL_CONVERSATION: 'SET_ALL_CONVERSATION',
|
||||
|
||||
@@ -14,6 +14,7 @@ class BaseActionCableConnector {
|
||||
);
|
||||
this.app = app;
|
||||
this.events = {};
|
||||
this.isAValidEvent = () => true;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -21,8 +22,10 @@ class BaseActionCableConnector {
|
||||
}
|
||||
|
||||
onReceived = ({ event, data } = {}) => {
|
||||
if (this.events[event] && typeof this.events[event] === 'function') {
|
||||
this.events[event](data);
|
||||
if (this.isAValidEvent(data)) {
|
||||
if (this.events[event] && typeof this.events[event] === 'function') {
|
||||
this.events[event](data);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user