mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 02:32:29 +00:00
chore: Custom Roles to manage permissions [ UI ] (#9865)
In admin settings, this Pr will add the UI for managing custom roles ( ref: https://github.com/chatwoot/chatwoot/pull/9995 ). It also handles the routing logic changes to accommodate fine-tuned permissions. --------- Co-authored-by: Pranav <pranavrajs@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
return if Current.account_user.administrator?
|
||||
|
||||
raise Pundit::NotAuthorizedError
|
||||
end
|
||||
|
||||
def common_params
|
||||
@@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
|
||||
|
||||
9
app/javascript/dashboard/api/customRole.js
Normal file
9
app/javascript/dashboard/api/customRole.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CustomRole extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_roles', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomRole();
|
||||
@@ -27,6 +27,11 @@ import {
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
import {
|
||||
getUserPermissions,
|
||||
filterItemsByPermission,
|
||||
} from 'dashboard/helper/permissionsHelper.js';
|
||||
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -204,6 +209,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
@@ -243,20 +249,19 @@ export default {
|
||||
name,
|
||||
};
|
||||
},
|
||||
userPermissions() {
|
||||
return getUserPermissions(this.currentUser, this.currentAccountId);
|
||||
},
|
||||
assigneeTabItems() {
|
||||
const ASSIGNEE_TYPE_TAB_KEYS = {
|
||||
me: 'mineCount',
|
||||
unassigned: 'unAssignedCount',
|
||||
all: 'allCount',
|
||||
};
|
||||
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
|
||||
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
|
||||
return {
|
||||
return filterItemsByPermission(
|
||||
ASSIGNEE_TYPE_TAB_PERMISSIONS,
|
||||
this.userPermissions,
|
||||
item => item.permissions
|
||||
).map(({ key, count: countKey }) => ({
|
||||
key,
|
||||
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
||||
count,
|
||||
};
|
||||
});
|
||||
count: this.conversationStats[countKey] || 0,
|
||||
}));
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return (
|
||||
|
||||
@@ -39,6 +39,7 @@ const settings = accountId => ({
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'sla_list',
|
||||
'custom_roles_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -178,6 +179,18 @@ const settings = accountId => ({
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
},
|
||||
{
|
||||
icon: 'scan-person',
|
||||
label: 'CUSTOM_ROLES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
|
||||
toStateName: 'custom_roles_list',
|
||||
isEnterpriseOnly: true,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
|
||||
@@ -52,9 +52,13 @@ export default {
|
||||
{{ account.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
|
||||
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
|
||||
>
|
||||
{{ account.role }}
|
||||
{{
|
||||
account.custom_role_id
|
||||
? account.custom_role.name
|
||||
: account.role
|
||||
}}
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
|
||||
@@ -21,8 +21,11 @@ const activeTabIndex = computed(() => {
|
||||
});
|
||||
|
||||
const onTabChange = selectedTabIndex => {
|
||||
if (props.items[selectedTabIndex].key !== props.activeTab) {
|
||||
emit('chatTabChange', props.items[selectedTabIndex].key);
|
||||
if (selectedTabIndex >= 0 && selectedTabIndex < props.items.length) {
|
||||
const selectedItem = props.items[selectedTabIndex];
|
||||
if (selectedItem.key !== props.activeTab) {
|
||||
emit('chatTabChange', selectedItem.key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +35,8 @@ const keyboardEvents = {
|
||||
if (props.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
onTabChange(0);
|
||||
} else {
|
||||
onTabChange(activeTabIndex.value + 1);
|
||||
const nextIndex = (activeTabIndex.value + 1) % props.items.length;
|
||||
onTabChange(nextIndex);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@ export default {
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
focusOnMount: { type: Boolean, default: true },
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
@@ -346,7 +347,9 @@ export default {
|
||||
mounted() {
|
||||
this.createEditorView();
|
||||
this.editorView.updateState(this.state);
|
||||
if (this.focusOnMount) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
|
||||
// BUS Event to insert text or markdown into the editor at the
|
||||
// current cursor position.
|
||||
@@ -383,7 +386,7 @@ export default {
|
||||
// these drafts can also have a signature, so we need to check if the body is empty
|
||||
// and handle things accordingly
|
||||
this.handleEmptyBodyWithSignature();
|
||||
} else {
|
||||
} else if (this.focusOnMount) {
|
||||
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
|
||||
// the position is set to start, because the signature is added at the end of the body
|
||||
this.focusEditorInputField('end');
|
||||
|
||||
53
app/javascript/dashboard/constants/permissions.js
Normal file
53
app/javascript/dashboard/constants/permissions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
export const AVAILABLE_CUSTOM_ROLE_PERMISSIONS = [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
];
|
||||
|
||||
export const ROLES = ['agent', 'administrator'];
|
||||
|
||||
export const CONVERSATION_PERMISSIONS = [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
];
|
||||
|
||||
export const MANAGE_ALL_CONVERSATION_PERMISSIONS = 'conversation_manage';
|
||||
|
||||
export const CONVERSATION_UNASSIGNED_PERMISSIONS =
|
||||
'conversation_unassigned_manage';
|
||||
|
||||
export const CONVERSATION_PARTICIPATING_PERMISSIONS =
|
||||
'conversation_participating_manage';
|
||||
|
||||
export const CONTACT_PERMISSIONS = 'contact_manage';
|
||||
|
||||
export const REPORTS_PERMISSIONS = 'report_manage';
|
||||
|
||||
export const PORTAL_PERMISSIONS = 'knowledge_base_manage';
|
||||
|
||||
export const ASSIGNEE_TYPE_TAB_PERMISSIONS = {
|
||||
me: {
|
||||
count: 'mineCount',
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
unassigned: {
|
||||
count: 'unAssignedCount',
|
||||
permissions: [
|
||||
...ROLES,
|
||||
MANAGE_ALL_CONVERSATION_PERMISSIONS,
|
||||
CONVERSATION_UNASSIGNED_PERMISSIONS,
|
||||
],
|
||||
},
|
||||
all: {
|
||||
count: 'allCount',
|
||||
permissions: [
|
||||
...ROLES,
|
||||
MANAGE_ALL_CONVERSATION_PERMISSIONS,
|
||||
CONVERSATION_PARTICIPATING_PERMISSIONS,
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -31,4 +31,5 @@ export const FEATURE_FLAGS = {
|
||||
IP_LOOKUP: 'ip_lookup',
|
||||
LINEAR: 'linear_integration',
|
||||
CAPTAIN: 'captain_integration',
|
||||
CUSTOM_ROLES: 'custom_roles',
|
||||
};
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
getAlertAudio,
|
||||
initOnEvents,
|
||||
} from 'shared/helpers/AudioNotificationHelper';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper.js';
|
||||
|
||||
const NOTIFICATION_TIME = 30000;
|
||||
|
||||
@@ -14,12 +19,13 @@ class DashboardAudioNotificationHelper {
|
||||
this.audioAlertType = 'none';
|
||||
this.playAlertOnlyWhenHidden = true;
|
||||
this.alertIfUnreadConversationExist = false;
|
||||
this.currentUser = null;
|
||||
this.currentUserId = null;
|
||||
this.audioAlertTone = 'ding';
|
||||
}
|
||||
|
||||
setInstanceValues = ({
|
||||
currentUserId,
|
||||
currentUser,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType,
|
||||
@@ -28,7 +34,8 @@ class DashboardAudioNotificationHelper {
|
||||
this.audioAlertType = audioAlertType;
|
||||
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.currentUserId = currentUserId;
|
||||
this.currentUser = currentUser;
|
||||
this.currentUserId = currentUser.id;
|
||||
this.audioAlertTone = audioAlertTone;
|
||||
initOnEvents.forEach(e => {
|
||||
document.addEventListener(e, this.onAudioListenEvent, false);
|
||||
@@ -112,6 +119,20 @@ class DashboardAudioNotificationHelper {
|
||||
return message?.sender_id === this.currentUserId;
|
||||
};
|
||||
|
||||
isUserHasConversationPermission = () => {
|
||||
const currentAccountId = window.WOOT.$store.getters.getCurrentAccountId;
|
||||
// Get the user permissions for the current account
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
currentAccountId
|
||||
);
|
||||
// Check if the user has the required permissions
|
||||
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
|
||||
permission => userPermissions.includes(permission)
|
||||
);
|
||||
return hasRequiredPermission;
|
||||
};
|
||||
|
||||
shouldNotifyOnMessage = message => {
|
||||
if (this.audioAlertType === 'mine') {
|
||||
return this.isConversationAssignedToCurrentUser(message);
|
||||
@@ -120,6 +141,11 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
|
||||
onNewMessage = message => {
|
||||
// If the user does not have the permission to view the conversation, then dismiss the alert
|
||||
if (!this.isUserHasConversationPermission()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is sent by the current user or the
|
||||
// correct notification is not enabled, then dismiss the alert
|
||||
if (
|
||||
|
||||
@@ -41,3 +41,32 @@ export const buildPermissionsFromRouter = (routes = []) =>
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Filters and transforms items based on user permissions.
|
||||
*
|
||||
* @param {Object} items - An object containing items to be filtered.
|
||||
* @param {Array} userPermissions - Array of permissions the user has.
|
||||
* @param {Function} getPermissions - Function to extract required permissions from an item.
|
||||
* @param {Function} [transformItem] - Optional function to transform each item after filtering.
|
||||
* @returns {Array} Filtered and transformed items.
|
||||
*/
|
||||
export const filterItemsByPermission = (
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
transformItem = (key, item) => ({ key, ...item })
|
||||
) => {
|
||||
// Helper function to check if an item has the required permissions
|
||||
const hasRequiredPermissions = item => {
|
||||
const requiredPermissions = getPermissions(item);
|
||||
return (
|
||||
requiredPermissions.length === 0 ||
|
||||
hasPermissions(requiredPermissions, userPermissions)
|
||||
);
|
||||
};
|
||||
|
||||
return Object.entries(items)
|
||||
.filter(([, item]) => hasRequiredPermissions(item)) // Keep only items with required permissions
|
||||
.map(([key, item]) => transformItem(key, item)); // Transform each remaining item
|
||||
};
|
||||
|
||||
@@ -4,11 +4,39 @@ import {
|
||||
getCurrentAccount,
|
||||
} from './permissionsHelper';
|
||||
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
REPORTS_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
return hasPermissions(routePermissions, userPermissions);
|
||||
};
|
||||
|
||||
export const defaultRedirectPage = (to, permissions) => {
|
||||
const { accountId } = to.params;
|
||||
|
||||
const permissionRoutes = [
|
||||
{
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
path: 'dashboard',
|
||||
},
|
||||
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
|
||||
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
|
||||
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
|
||||
];
|
||||
|
||||
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
|
||||
hasPermissions(routePermissions, permissions)
|
||||
);
|
||||
|
||||
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
// If the current account is active, then check for the route permissions
|
||||
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
||||
@@ -22,7 +50,7 @@ const validateActiveAccountRoutes = (to, user) => {
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
|
||||
};
|
||||
|
||||
export const validateLoggedInRoutes = (to, user) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const initializeAudioAlerts = user => {
|
||||
} = uiSettings || {};
|
||||
|
||||
DashboardAudioNotificationHelper.setInstanceValues({
|
||||
currentUserId: user.id,
|
||||
currentUser: user,
|
||||
audioAlertType: audioAlertType || 'none',
|
||||
audioAlertTone: audioAlertTone || 'ding',
|
||||
alwaysPlayAudioAlert: alwaysPlayAudioAlert || false,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getCurrentAccount,
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
filterItemsByPermission,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
@@ -105,3 +106,113 @@ describe('buildPermissionsFromRouter', () => {
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterItemsByPermission', () => {
|
||||
const items = {
|
||||
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
|
||||
item2: {
|
||||
name: 'Item 2',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
],
|
||||
},
|
||||
item3: { name: 'Item 3', permissions: ['contact_manage'] },
|
||||
item4: { name: 'Item 4', permissions: ['report_manage'] },
|
||||
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
|
||||
item6: {
|
||||
name: 'Item 6',
|
||||
permissions: [
|
||||
'agent',
|
||||
'administrator',
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
},
|
||||
item7: { name: 'Item 7', permissions: [] },
|
||||
};
|
||||
|
||||
const getPermissions = item => item.permissions;
|
||||
|
||||
it('filters items based on user permissions', () => {
|
||||
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item3', name: 'Item 3' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item4', name: 'Item 4' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item6', name: 'Item 6' })
|
||||
);
|
||||
});
|
||||
|
||||
it('includes items with empty permissions', () => {
|
||||
const userPermissions = [];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item7', name: 'Item 7' })
|
||||
);
|
||||
});
|
||||
|
||||
it('uses custom transform function when provided', () => {
|
||||
const userPermissions = ['agent', 'contact_manage'];
|
||||
const customTransform = (key, item) => ({ id: key, title: item.name });
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
customTransform
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
|
||||
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
|
||||
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
|
||||
});
|
||||
|
||||
it('handles empty items object', () => {
|
||||
const result = filterItemsByPermission({}, ['agent'], getPermissions);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles custom getPermissions function', () => {
|
||||
const customItems = {
|
||||
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
|
||||
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
|
||||
};
|
||||
const customGetPermissions = item => item.requiredPerms;
|
||||
const result = filterItemsByPermission(
|
||||
customItems,
|
||||
['agent'],
|
||||
customGetPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
isAConversationRoute,
|
||||
defaultRedirectPage,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
@@ -14,6 +15,57 @@ describe('#routeIsAccessibleFor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultRedirectPage', () => {
|
||||
const to = {
|
||||
params: { accountId: '2' },
|
||||
fullPath: '/app/accounts/2/dashboard',
|
||||
name: 'home',
|
||||
};
|
||||
|
||||
it('should return dashboard route for users with conversation permissions', () => {
|
||||
const permissions = ['conversation_manage', 'agent'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return contacts route for users with contact permissions', () => {
|
||||
const permissions = ['contact_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
|
||||
});
|
||||
|
||||
it('should return reports route for users with report permissions', () => {
|
||||
const permissions = ['report_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe(
|
||||
'accounts/2/reports/overview'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return portals route for users with portal permissions', () => {
|
||||
const permissions = ['knowledge_base_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
|
||||
});
|
||||
|
||||
it('should return dashboard route as default for users with custom roles', () => {
|
||||
const permissions = ['custom_role'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with administrator role', () => {
|
||||
const permissions = ['administrator'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with multiple permissions', () => {
|
||||
const permissions = [
|
||||
'contact_manage',
|
||||
'custom_role',
|
||||
'conversation_manage',
|
||||
'agent',
|
||||
'administrator',
|
||||
];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "Status",
|
||||
"ACTIONS": "Actions",
|
||||
"VERIFIED": "Verified",
|
||||
"VERIFICATION_PENDING": "Verification Pending"
|
||||
"VERIFICATION_PENDING": "Verification Pending",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add agent to your team",
|
||||
|
||||
86
app/javascript/dashboard/i18n/locale/en/customRole.json
Normal file
86
app/javascript/dashboard/i18n/locale/en/customRole.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "There are no items matching this query.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": ["Name", "Description", "Permissions", "Actions"]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "Name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "Description is required."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Submit",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Update",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import sla from './sla.json';
|
||||
import inbox from './inbox.json';
|
||||
import general from './general.json';
|
||||
import datePicker from './datePicker.json';
|
||||
import customRole from './customRole.json';
|
||||
|
||||
export default {
|
||||
...advancedFilters,
|
||||
@@ -70,4 +71,5 @@ export default {
|
||||
...inbox,
|
||||
...general,
|
||||
...datePicker,
|
||||
...customRole,
|
||||
};
|
||||
|
||||
@@ -145,11 +145,7 @@
|
||||
},
|
||||
"AVAILABILITY": {
|
||||
"LABEL": "Availability",
|
||||
"STATUSES_LIST": [
|
||||
"Online",
|
||||
"Busy",
|
||||
"Offline"
|
||||
],
|
||||
"STATUSES_LIST": ["Online", "Busy", "Offline"],
|
||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||
},
|
||||
@@ -277,6 +273,7 @@
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"SLA": "SLA",
|
||||
"CUSTOM_ROLES": "Custom Roles",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
|
||||
@@ -4,6 +4,16 @@ import SearchTabs from './SearchTabs.vue';
|
||||
import SearchResultConversationsList from './SearchResultConversationsList.vue';
|
||||
import SearchResultMessagesList from './SearchResultMessagesList.vue';
|
||||
import SearchResultContactsList from './SearchResultContactsList.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
import {
|
||||
getUserPermissions,
|
||||
filterItemsByPermission,
|
||||
} from 'dashboard/helper/permissionsHelper.js';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
@@ -14,16 +24,22 @@ export default {
|
||||
SearchResultContactsList,
|
||||
SearchResultConversationsList,
|
||||
SearchResultMessagesList,
|
||||
Policy,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 'all',
|
||||
query: '',
|
||||
contactPermissions: CONTACT_PERMISSIONS,
|
||||
conversationPermissions: CONVERSATION_PERMISSIONS,
|
||||
rolePermissions: ROLES,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
contactRecords: 'conversationSearch/getContactRecords',
|
||||
conversationRecords: 'conversationSearch/getConversationRecords',
|
||||
messageRecords: 'conversationSearch/getMessageRecords',
|
||||
@@ -59,34 +75,76 @@ export default {
|
||||
filterMessages() {
|
||||
return this.selectedTab === 'messages' || this.isSelectedTabAll;
|
||||
},
|
||||
userPermissions() {
|
||||
return getUserPermissions(this.currentUser, this.currentAccountId);
|
||||
},
|
||||
totalSearchResultsCount() {
|
||||
return (
|
||||
this.contacts.length + this.conversations.length + this.messages.length
|
||||
const permissionCounts = {
|
||||
contacts: {
|
||||
permissions: [...this.rolePermissions, this.contactPermissions],
|
||||
count: () => this.contacts.length,
|
||||
},
|
||||
conversations: {
|
||||
permissions: [
|
||||
...this.rolePermissions,
|
||||
...this.conversationPermissions,
|
||||
],
|
||||
count: () => this.conversations.length + this.messages.length,
|
||||
},
|
||||
};
|
||||
|
||||
const filteredCounts = filterItemsByPermission(
|
||||
permissionCounts,
|
||||
this.userPermissions,
|
||||
item => item.permissions,
|
||||
(_, item) => item.count
|
||||
);
|
||||
|
||||
return filteredCounts.reduce((total, count) => total + count(), 0);
|
||||
},
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
const allTabsConfig = {
|
||||
all: {
|
||||
key: 'all',
|
||||
name: this.$t('SEARCH.TABS.ALL'),
|
||||
count: this.totalSearchResultsCount,
|
||||
permissions: [
|
||||
this.contactPermissions,
|
||||
...this.rolePermissions,
|
||||
...this.conversationPermissions,
|
||||
],
|
||||
},
|
||||
{
|
||||
contacts: {
|
||||
key: 'contacts',
|
||||
name: this.$t('SEARCH.TABS.CONTACTS'),
|
||||
count: this.contacts.length,
|
||||
permissions: [...this.rolePermissions, this.contactPermissions],
|
||||
},
|
||||
{
|
||||
conversations: {
|
||||
key: 'conversations',
|
||||
name: this.$t('SEARCH.TABS.CONVERSATIONS'),
|
||||
count: this.conversations.length,
|
||||
permissions: [
|
||||
...this.rolePermissions,
|
||||
...this.conversationPermissions,
|
||||
],
|
||||
},
|
||||
{
|
||||
messages: {
|
||||
key: 'messages',
|
||||
name: this.$t('SEARCH.TABS.MESSAGES'),
|
||||
count: this.messages.length,
|
||||
permissions: [
|
||||
...this.rolePermissions,
|
||||
...this.conversationPermissions,
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return filterItemsByPermission(
|
||||
allTabsConfig,
|
||||
this.userPermissions,
|
||||
item => item.permissions
|
||||
);
|
||||
},
|
||||
activeTabIndex() {
|
||||
const index = this.tabs.findIndex(tab => tab.key === this.selectedTab);
|
||||
@@ -165,6 +223,7 @@ export default {
|
||||
</header>
|
||||
<div class="search-results">
|
||||
<div v-if="showResultsSection">
|
||||
<Policy :permissions="[...rolePermissions, contactPermissions]">
|
||||
<SearchResultContactsList
|
||||
v-if="filterContacts"
|
||||
:is-fetching="uiFlags.contact.isFetching"
|
||||
@@ -172,7 +231,11 @@ export default {
|
||||
:query="query"
|
||||
:show-title="isSelectedTabAll"
|
||||
/>
|
||||
</Policy>
|
||||
|
||||
<Policy
|
||||
:permissions="[...rolePermissions, ...conversationPermissions]"
|
||||
>
|
||||
<SearchResultMessagesList
|
||||
v-if="filterMessages"
|
||||
:is-fetching="uiFlags.message.isFetching"
|
||||
@@ -180,7 +243,11 @@ export default {
|
||||
:query="query"
|
||||
:show-title="isSelectedTabAll"
|
||||
/>
|
||||
</Policy>
|
||||
|
||||
<Policy
|
||||
:permissions="[...rolePermissions, ...conversationPermissions]"
|
||||
>
|
||||
<SearchResultConversationsList
|
||||
v-if="filterConversations"
|
||||
:is-fetching="uiFlags.conversation.isFetching"
|
||||
@@ -188,6 +255,7 @@ export default {
|
||||
:query="query"
|
||||
:show-title="isSelectedTabAll"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
<div v-else-if="showEmptySearchResults" class="empty">
|
||||
<fluent-icon icon="info" size="16px" class="icon" />
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* eslint-disable storybook/default-exports */
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
const SearchView = () => import('./components/SearchView.vue');
|
||||
|
||||
@@ -8,7 +13,7 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/search'),
|
||||
name: 'search',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS, CONTACT_PERMISSIONS],
|
||||
},
|
||||
component: SearchView,
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/contacts'),
|
||||
name: 'contacts_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
},
|
||||
@@ -16,7 +16,7 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
|
||||
name: 'contacts_segments_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
@@ -27,7 +27,7 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||
name: 'contacts_labels_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
@@ -38,7 +38,7 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contact_profile_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactManageView,
|
||||
props: route => {
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
const ConversationView = () => import('./ConversationView.vue');
|
||||
|
||||
const CONVERSATION_PERMISSIONS = [
|
||||
'administrator',
|
||||
'agent',
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
];
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/dashboard'),
|
||||
name: 'home',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => {
|
||||
@@ -19,7 +27,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
|
||||
name: 'inbox_conversation',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
@@ -30,7 +38,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
|
||||
name: 'inbox_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
@@ -43,7 +51,7 @@ export default {
|
||||
),
|
||||
name: 'conversation_through_inbox',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
@@ -57,7 +65,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/label/:label'),
|
||||
name: 'label_conversations',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ label: route.params.label }),
|
||||
@@ -68,7 +76,7 @@ export default {
|
||||
),
|
||||
name: 'conversations_through_label',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
@@ -80,7 +88,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/team/:teamId'),
|
||||
name: 'team_conversations',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ teamId: route.params.teamId }),
|
||||
@@ -91,7 +99,7 @@ export default {
|
||||
),
|
||||
name: 'conversations_through_team',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
@@ -103,7 +111,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/custom_view/:id'),
|
||||
name: 'folder_conversations',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ foldersId: route.params.id }),
|
||||
@@ -114,7 +122,7 @@ export default {
|
||||
),
|
||||
name: 'conversations_through_folders',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
@@ -126,7 +134,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/mentions/conversations'),
|
||||
name: 'conversation_mentions',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'mention' }),
|
||||
@@ -137,7 +145,7 @@ export default {
|
||||
),
|
||||
name: 'conversation_through_mentions',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
@@ -149,7 +157,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/unattended/conversations'),
|
||||
name: 'conversation_unattended',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'unattended' }),
|
||||
@@ -160,7 +168,7 @@ export default {
|
||||
),
|
||||
name: 'conversation_through_unattended',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
@@ -172,7 +180,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/participating/conversations'),
|
||||
name: 'conversation_participating',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'participating' }),
|
||||
@@ -183,7 +191,7 @@ export default {
|
||||
),
|
||||
name: 'conversation_through_participating',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/suspended'),
|
||||
name: 'account_suspended',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
},
|
||||
component: Suspended,
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ const portalRoutes = [
|
||||
path: getPortalRoute(''),
|
||||
name: 'default_portal_articles',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
component: DefaultPortalArticles,
|
||||
},
|
||||
@@ -41,7 +41,7 @@ const portalRoutes = [
|
||||
path: getPortalRoute('all'),
|
||||
name: 'list_all_portals',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllPortals,
|
||||
},
|
||||
@@ -54,7 +54,7 @@ const portalRoutes = [
|
||||
name: 'new_portal_information',
|
||||
component: PortalDetails,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@ const portalRoutes = [
|
||||
name: 'portal_customization',
|
||||
component: PortalCustomization,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -70,7 +70,7 @@ const portalRoutes = [
|
||||
name: 'portal_finish',
|
||||
component: PortalSettingsFinish,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -79,14 +79,14 @@ const portalRoutes = [
|
||||
path: getPortalRoute(':portalSlug'),
|
||||
name: 'portalSlug',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ShowPortal,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/edit'),
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditPortal,
|
||||
children: [
|
||||
@@ -95,7 +95,7 @@ const portalRoutes = [
|
||||
name: 'edit_portal_information',
|
||||
component: EditPortalBasic,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,7 +103,7 @@ const portalRoutes = [
|
||||
name: 'edit_portal_customization',
|
||||
component: EditPortalCustomization,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,14 +111,14 @@ const portalRoutes = [
|
||||
name: 'edit_portal_locales',
|
||||
component: EditPortalLocales,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'list_all_locale_categories',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
@@ -131,7 +131,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles'),
|
||||
name: 'list_all_locale_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
@@ -139,7 +139,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
||||
name: 'new_article',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: NewArticle,
|
||||
},
|
||||
@@ -147,7 +147,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
|
||||
name: 'list_mine_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
@@ -155,7 +155,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
|
||||
name: 'list_archived_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
@@ -164,7 +164,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
|
||||
name: 'list_draft_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
@@ -173,7 +173,7 @@ const articleRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
|
||||
name: 'edit_article',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditArticle,
|
||||
},
|
||||
@@ -184,7 +184,7 @@ const categoryRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/categories'),
|
||||
name: 'all_locale_categories',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
@@ -192,7 +192,7 @@ const categoryRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/new'),
|
||||
name: 'new_category_in_locale',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: NewCategory,
|
||||
},
|
||||
@@ -200,7 +200,7 @@ const categoryRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'show_category',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
@@ -210,7 +210,7 @@ const categoryRoutes = [
|
||||
),
|
||||
name: 'show_category_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListCategoryArticles,
|
||||
},
|
||||
@@ -218,7 +218,7 @@ const categoryRoutes = [
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'edit_category',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditCategory,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,10 @@ import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
const InboxListView = () => import('./InboxList.vue');
|
||||
const InboxDetailView = () => import('./InboxView.vue');
|
||||
const InboxEmptyStateView = () => import('./InboxEmptyState.vue');
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
@@ -13,7 +17,7 @@ export const routes = [
|
||||
name: 'inbox_view',
|
||||
component: InboxEmptyStateView,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -21,7 +25,7 @@ export const routes = [
|
||||
name: 'inbox_view_conversation',
|
||||
component: InboxDetailView,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ export const routes = [
|
||||
name: 'notifications_index',
|
||||
component: NotificationsView,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,68 +1,85 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, email } from '@vuelidate/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agentName: '',
|
||||
agentEmail: '',
|
||||
agentType: 'agent',
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
roles: [
|
||||
{
|
||||
name: 'administrator',
|
||||
label: this.$t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
|
||||
},
|
||||
{
|
||||
name: 'agent',
|
||||
label: this.$t('AGENT_MGMT.AGENT_TYPES.AGENT'),
|
||||
},
|
||||
],
|
||||
show: true,
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agentName = ref('');
|
||||
const agentEmail = ref('');
|
||||
const selectedRoleId = ref('agent');
|
||||
|
||||
const rules = {
|
||||
agentName: { required },
|
||||
agentEmail: { required, email },
|
||||
selectedRoleId: { required },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
}),
|
||||
},
|
||||
validations: {
|
||||
agentName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
agentEmail: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
agentType: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async addAgent() {
|
||||
try {
|
||||
await this.$store.dispatch('agents/create', {
|
||||
name: this.agentName,
|
||||
email: this.agentEmail,
|
||||
role: this.agentType,
|
||||
const v$ = useVuelidate(rules, {
|
||||
agentName,
|
||||
agentEmail,
|
||||
selectedRoleId,
|
||||
});
|
||||
useAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
|
||||
const uiFlags = useMapGetter('agents/getUIFlags');
|
||||
const getCustomRoles = useMapGetter('customRole/getCustomRoles');
|
||||
|
||||
const roles = computed(() => {
|
||||
const defaultRoles = [
|
||||
{
|
||||
id: 'administrator',
|
||||
name: 'administrator',
|
||||
label: t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'agent',
|
||||
label: t('AGENT_MGMT.AGENT_TYPES.AGENT'),
|
||||
},
|
||||
];
|
||||
|
||||
const customRoles = getCustomRoles.value.map(role => ({
|
||||
id: role.id,
|
||||
name: `custom_${role.id}`,
|
||||
label: role.name,
|
||||
}));
|
||||
|
||||
return [...defaultRoles, ...customRoles];
|
||||
});
|
||||
|
||||
const selectedRole = computed(() =>
|
||||
roles.value.find(
|
||||
role =>
|
||||
role.id === selectedRoleId.value || role.name === selectedRoleId.value
|
||||
)
|
||||
);
|
||||
|
||||
const addAgent = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name: agentName.value,
|
||||
email: agentEmail.value,
|
||||
};
|
||||
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
}
|
||||
|
||||
await store.dispatch('agents/create', payload);
|
||||
useAlert(t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
const {
|
||||
response: {
|
||||
@@ -76,29 +93,22 @@ export default {
|
||||
|
||||
let errorMessage = '';
|
||||
if (error?.response?.status === 422 && !attributes.includes('base')) {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
errorMessage = t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
} else {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
errorMessage = t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
}
|
||||
useAlert(errorResponse || attrError || errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('AGENT_MGMT.ADD.TITLE')"
|
||||
:header-content="$t('AGENT_MGMT.ADD.DESC')"
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex flex-col items-start w-full"
|
||||
@submit.prevent="addAgent()"
|
||||
>
|
||||
<form class="flex flex-col items-start w-full" @submit.prevent="addAgent">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
|
||||
@@ -110,47 +120,45 @@ export default {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentType.$error }">
|
||||
<label :class="{ error: v$.selectedRoleId.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.LABEL') }}
|
||||
<select v-model="agentType">
|
||||
<option v-for="role in roles" :key="role.name" :value="role.name">
|
||||
<select v-model="selectedRoleId" @change="v$.selectedRoleId.$touch">
|
||||
<option v-for="role in roles" :key="role.id" :value="role.id">
|
||||
{{ role.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentType.$error" class="message">
|
||||
<span v-if="v$.selectedRoleId.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentEmail.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="agentEmail"
|
||||
type="text"
|
||||
type="email"
|
||||
:placeholder="$t('AGENT_MGMT.ADD.FORM.EMAIL.PLACEHOLDER')"
|
||||
@input="v$.agentEmail.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
v$.agentEmail.$invalid ||
|
||||
v$.agentName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
<WootSubmitButton
|
||||
:disabled="v$.$invalid || uiFlags.isCreating"
|
||||
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('AGENT_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vue';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
import Auth from '../../../../api/auth';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@@ -36,99 +30,123 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
customRoleId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roles: [
|
||||
{
|
||||
name: 'administrator',
|
||||
label: this.$t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
|
||||
},
|
||||
{
|
||||
name: 'agent',
|
||||
label: this.$t('AGENT_MGMT.AGENT_TYPES.AGENT'),
|
||||
},
|
||||
],
|
||||
agentName: this.name,
|
||||
agentAvailability: this.availability,
|
||||
agentType: this.type,
|
||||
agentCredentials: {
|
||||
email: this.email,
|
||||
},
|
||||
show: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agentName = ref(props.name);
|
||||
const agentAvailability = ref(props.availability);
|
||||
const selectedRoleId = ref(props.customRoleId || props.type);
|
||||
const agentCredentials = ref({ email: props.email });
|
||||
|
||||
const rules = {
|
||||
agentName: { required, minLength: minLength(1) },
|
||||
selectedRoleId: { required },
|
||||
agentAvailability: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, {
|
||||
agentName,
|
||||
selectedRoleId,
|
||||
agentAvailability,
|
||||
});
|
||||
|
||||
const pageTitle = computed(
|
||||
() => `${t('AGENT_MGMT.EDIT.TITLE')} - ${props.name}`
|
||||
);
|
||||
|
||||
const uiFlags = useMapGetter('agents/getUIFlags');
|
||||
const getCustomRoles = useMapGetter('customRole/getCustomRoles');
|
||||
|
||||
const roles = computed(() => {
|
||||
const defaultRoles = [
|
||||
{
|
||||
id: 'administrator',
|
||||
name: 'administrator',
|
||||
label: t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
|
||||
},
|
||||
validations: {
|
||||
agentName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'agent',
|
||||
label: t('AGENT_MGMT.AGENT_TYPES.AGENT'),
|
||||
},
|
||||
agentType: {
|
||||
required,
|
||||
},
|
||||
agentAvailability: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
return `${this.$t('AGENT_MGMT.EDIT.TITLE')} - ${this.name}`;
|
||||
},
|
||||
...mapGetters({
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
}),
|
||||
availabilityStatuses() {
|
||||
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
|
||||
];
|
||||
|
||||
const customRoles = getCustomRoles.value.map(role => ({
|
||||
id: role.id,
|
||||
name: `custom_${role.id}`,
|
||||
label: role.name,
|
||||
}));
|
||||
|
||||
return [...defaultRoles, ...customRoles];
|
||||
});
|
||||
|
||||
const selectedRole = computed(() =>
|
||||
roles.value.find(
|
||||
role =>
|
||||
role.id === selectedRoleId.value || role.name === selectedRoleId.value
|
||||
)
|
||||
);
|
||||
|
||||
const availabilityStatuses = computed(() =>
|
||||
t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
|
||||
(statusLabel, index) => ({
|
||||
label: statusLabel,
|
||||
value: AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled:
|
||||
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled: props.availability === AVAILABILITY_STATUS_KEYS[index],
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async editAgent() {
|
||||
|
||||
const editAgent = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('agents/update', {
|
||||
id: this.id,
|
||||
name: this.agentName,
|
||||
role: this.agentType,
|
||||
availability: this.agentAvailability,
|
||||
});
|
||||
useAlert(this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
useAlert(this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
const payload = {
|
||||
id: props.id,
|
||||
name: agentName.value,
|
||||
availability: agentAvailability.value,
|
||||
};
|
||||
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
payload.custom_role_id = null;
|
||||
}
|
||||
},
|
||||
async resetPassword() {
|
||||
|
||||
await store.dispatch('agents/update', payload);
|
||||
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
useAlert(t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = async () => {
|
||||
try {
|
||||
await Auth.resetPassword(this.agentCredentials);
|
||||
useAlert(
|
||||
this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE')
|
||||
);
|
||||
await Auth.resetPassword(agentCredentials.value);
|
||||
useAlert(t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ERROR_MESSAGE'));
|
||||
useAlert(t('AGENT_MGMT.EDIT.PASSWORD_RESET.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show.sync="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="pageTitle" />
|
||||
<form class="w-full" @submit.prevent="editAgent()">
|
||||
<form class="w-full" @submit.prevent="editAgent">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
|
||||
@@ -142,14 +160,14 @@ export default {
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentType.$error }">
|
||||
<label :class="{ error: v$.selectedRoleId.$error }">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.LABEL') }}
|
||||
<select v-model="agentType">
|
||||
<option v-for="role in roles" :key="role.name" :value="role.name">
|
||||
<select v-model="selectedRoleId" @change="v$.selectedRoleId.$touch">
|
||||
<option v-for="role in roles" :key="role.id" :value="role.id">
|
||||
{{ role.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentType.$error" class="message">
|
||||
<span v-if="v$.selectedRoleId.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
@@ -158,13 +176,16 @@ export default {
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentAvailability.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.AVAILABILITY.LABEL') }}
|
||||
<select v-model="agentAvailability">
|
||||
<option
|
||||
v-for="role in availabilityStatuses"
|
||||
:key="role.value"
|
||||
:value="role.value"
|
||||
<select
|
||||
v-model="agentAvailability"
|
||||
@change="v$.agentAvailability.$touch"
|
||||
>
|
||||
{{ role.label }}
|
||||
<option
|
||||
v-for="status in availabilityStatuses"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentAvailability.$error" class="message">
|
||||
@@ -172,18 +193,15 @@ export default {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-[50%]">
|
||||
<WootSubmitButton
|
||||
:disabled="
|
||||
v$.agentType.$invalid ||
|
||||
v$.agentName.$invalid ||
|
||||
uiFlags.isUpdating
|
||||
"
|
||||
:disabled="v$.$invalid || uiFlags.isUpdating"
|
||||
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isUpdating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('AGENT_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -199,5 +217,4 @@ export default {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import {
|
||||
useStoreGetters,
|
||||
useStore,
|
||||
useMapGetter,
|
||||
} from 'dashboard/composables/store';
|
||||
|
||||
import AddAgent from './AddAgent.vue';
|
||||
import EditAgent from './EditAgent.vue';
|
||||
@@ -34,11 +38,32 @@ const deleteMessage = computed(() => {
|
||||
const agentList = computed(() => getters['agents/getAgents'].value);
|
||||
const uiFlags = computed(() => getters['agents/getUIFlags'].value);
|
||||
const currentUserId = computed(() => getters.getCurrentUserID.value);
|
||||
const customRoles = useMapGetter('customRole/getCustomRoles');
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('agents/get');
|
||||
store.dispatch('customRole/getCustomRole');
|
||||
});
|
||||
|
||||
const findCustomRole = agent =>
|
||||
customRoles.value.find(role => role.id === agent.custom_role_id);
|
||||
|
||||
const getAgentRoleName = agent => {
|
||||
if (!agent.custom_role_id) {
|
||||
return t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`);
|
||||
}
|
||||
const customRole = findCustomRole(agent);
|
||||
return customRole ? customRole.name : '';
|
||||
};
|
||||
|
||||
const getAgentRolePermissions = agent => {
|
||||
if (!agent.custom_role_id) {
|
||||
return [];
|
||||
}
|
||||
const customRole = findCustomRole(agent);
|
||||
return customRole?.permissions || [];
|
||||
};
|
||||
|
||||
const verifiedAdministrators = computed(() => {
|
||||
return agentList.value.filter(
|
||||
agent => agent.role === 'administrator' && agent.confirmed
|
||||
@@ -63,6 +88,7 @@ const showDeleteAction = agent => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const showAlertMessage = message => {
|
||||
loading.value[currentAgent.value.id] = false;
|
||||
currentAgent.value = {};
|
||||
@@ -124,7 +150,7 @@ const confirmDeletion = () => {
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
class="rounded-md button nice"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
@@ -140,7 +166,7 @@ const confirmDeletion = () => {
|
||||
>
|
||||
<tr v-for="(agent, index) in agentList" :key="agent.email">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center flex-row gap-4">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<Thumbnail
|
||||
:src="agent.thumbnail"
|
||||
:username="agent.name"
|
||||
@@ -156,9 +182,39 @@ const confirmDeletion = () => {
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span class="block font-medium capitalize">
|
||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`) }}
|
||||
<td class="relative py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span
|
||||
class="block font-medium w-fit"
|
||||
:class="{
|
||||
'hover:text-gray-900 group cursor-pointer':
|
||||
agent.custom_role_id,
|
||||
}"
|
||||
>
|
||||
{{ getAgentRoleName(agent) }}
|
||||
|
||||
<div
|
||||
class="absolute left-0 z-10 hidden max-w-[300px] w-auto bg-white rounded-xl border border-slate-50 shadow-lg top-14 md:top-12 dark:bg-slate-800 dark:border-slate-700"
|
||||
:class="{ 'group-hover:block': agent.custom_role_id }"
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-4">
|
||||
<span class="font-semibold">
|
||||
{{ $t('AGENT_MGMT.LIST.AVAILABLE_CUSTOM_ROLE') }}
|
||||
</span>
|
||||
<ul class="pl-4 mb-0 list-disc">
|
||||
<li
|
||||
v-for="permission in getAgentRolePermissions(agent)"
|
||||
:key="permission"
|
||||
class="font-normal"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
@@ -200,7 +256,7 @@ const confirmDeletion = () => {
|
||||
</template>
|
||||
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddAgent :on-close="hideAddPopup" />
|
||||
<AddAgent @close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
@@ -211,7 +267,8 @@ const confirmDeletion = () => {
|
||||
:type="currentAgent.role"
|
||||
:email="currentAgent.email"
|
||||
:availability="currentAgent.availability_status"
|
||||
:on-close="hideEditPopup"
|
||||
:custom-role-id="currentAgent.custom_role_id"
|
||||
@close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const CannedHome = () => import('./Index.vue');
|
||||
|
||||
@@ -17,7 +20,7 @@ export default {
|
||||
path: 'list',
|
||||
name: 'canned_list',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
component: CannedHome,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
featurePrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
i18nKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isOnChatwootCloud: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSuperAdmin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col max-w-md px-6 py-6 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-100 dark:border-slate-900"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2 mb-4">
|
||||
<span
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full bg-woot-75/70 dark:bg-woot-800/40"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base font-medium text-slate-900 dark:text-white">
|
||||
{{ $t(`${featurePrefix}.PAYWALL.TITLE`) }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-normal"
|
||||
v-html="$t(`${featurePrefix}.${i18nKey}.AVAILABLE_ON`)"
|
||||
/>
|
||||
<p class="text-sm font-normal">
|
||||
{{ $t(`${featurePrefix}.${i18nKey}.UPGRADE_PROMPT`) }}
|
||||
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
|
||||
{{ $t(`${featurePrefix}.ENTERPRISE_PAYWALL.ASK_ADMIN`) }}
|
||||
</span>
|
||||
</p>
|
||||
<template v-if="isOnChatwootCloud || true">
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
class="w-full mt-2 text-center rounded-xl"
|
||||
size="expanded"
|
||||
is-expanded
|
||||
@click="emit('click')"
|
||||
>
|
||||
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
||||
</woot-button>
|
||||
<span class="mt-2 text-xs tracking-tight text-center">
|
||||
{{ $t(`${featurePrefix}.PAYWALL.CANCEL_ANYTIME`) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="isSuperAdmin">
|
||||
<a href="/super_admin" class="block w-full">
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
class="w-full mt-2 text-center rounded-xl"
|
||||
size="expanded"
|
||||
is-expanded
|
||||
>
|
||||
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import CustomRoleModal from './component/CustomRoleModal.vue';
|
||||
import CustomRoleTableBody from './component/CustomRoleTableBody.vue';
|
||||
import CustomRolePaywall from './component/CustomRolePaywall.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const showCustomRoleModal = ref(false);
|
||||
const customRoleModalMode = ref('add');
|
||||
const selectedRole = ref(null);
|
||||
const loading = ref({});
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const activeResponse = ref({});
|
||||
|
||||
const records = useMapGetter('customRole/getCustomRoles');
|
||||
const uiFlags = useMapGetter('customRole/getUIFlags');
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
() => `${t('CUSTOM_ROLE.DELETE.CONFIRM.YES')} ${activeResponse.value.name}`
|
||||
);
|
||||
|
||||
const deleteRejectText = computed(
|
||||
() => `${t('CUSTOM_ROLE.DELETE.CONFIRM.NO')} ${activeResponse.value.name}`
|
||||
);
|
||||
|
||||
const deleteMessage = computed(() => {
|
||||
return ` ${activeResponse.value.name} ? `;
|
||||
});
|
||||
|
||||
const isFeatureEnabledOnAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
const isBehindAPaywall = computed(() => {
|
||||
return !isFeatureEnabledOnAccount.value(
|
||||
currentAccountId.value,
|
||||
'custom_roles'
|
||||
);
|
||||
});
|
||||
|
||||
const fetchCustomRoles = async () => {
|
||||
try {
|
||||
await store.dispatch('customRole/getCustomRole');
|
||||
} catch (error) {
|
||||
// Ignore Error
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCustomRoles();
|
||||
});
|
||||
|
||||
const showAlertMessage = message => {
|
||||
loading.value[activeResponse.value.id] = false;
|
||||
activeResponse.value = {};
|
||||
useAlert(message);
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
if (isBehindAPaywall.value) return;
|
||||
customRoleModalMode.value = 'add';
|
||||
selectedRole.value = null;
|
||||
showCustomRoleModal.value = true;
|
||||
};
|
||||
|
||||
const openEditModal = role => {
|
||||
customRoleModalMode.value = 'edit';
|
||||
selectedRole.value = role;
|
||||
showCustomRoleModal.value = true;
|
||||
};
|
||||
|
||||
const hideCustomRoleModal = () => {
|
||||
selectedRole.value = null;
|
||||
showCustomRoleModal.value = false;
|
||||
};
|
||||
|
||||
const openDeletePopup = response => {
|
||||
showDeleteConfirmationPopup.value = true;
|
||||
activeResponse.value = response;
|
||||
};
|
||||
|
||||
const closeDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = false;
|
||||
};
|
||||
|
||||
const deleteCustomRole = async id => {
|
||||
try {
|
||||
await store.dispatch('customRole/deleteCustomRole', id);
|
||||
showAlertMessage(t('CUSTOM_ROLE.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('CUSTOM_ROLE.DELETE.API.ERROR_MESSAGE');
|
||||
showAlertMessage(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
loading[activeResponse.value.id] = true;
|
||||
closeDeletePopup();
|
||||
deleteCustomRole(activeResponse.value.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:loading-message="$t('CUSTOM_ROLE.LOADING')"
|
||||
:no-records-found="!records.length && !isBehindAPaywall"
|
||||
:no-records-message="$t('CUSTOM_ROLE.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('CUSTOM_ROLE.HEADER')"
|
||||
:description="$t('CUSTOM_ROLE.DESCRIPTION')"
|
||||
:link-text="$t('CUSTOM_ROLE.LEARN_MORE')"
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="rounded-md button nice"
|
||||
icon="add-circle"
|
||||
:disabled="isBehindAPaywall"
|
||||
@click="openAddModal"
|
||||
>
|
||||
{{ $t('CUSTOM_ROLE.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<CustomRolePaywall v-if="isBehindAPaywall" />
|
||||
<table
|
||||
v-else
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t('CUSTOM_ROLE.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
class="py-4 pr-4 font-semibold text-left text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ thHeader }}
|
||||
</span>
|
||||
</th>
|
||||
</thead>
|
||||
|
||||
<CustomRoleTableBody
|
||||
:roles="records"
|
||||
:loading="loading"
|
||||
@edit="openEditModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<woot-modal
|
||||
:show.sync="showCustomRoleModal"
|
||||
:on-close="hideCustomRoleModal"
|
||||
>
|
||||
<CustomRoleModal
|
||||
:mode="customRoleModalMode"
|
||||
:selected-role="selectedRole"
|
||||
@close="hideCustomRoleModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('CUSTOM_ROLE.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('CUSTOM_ROLE.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
AVAILABLE_CUSTOM_ROLE_PERMISSIONS,
|
||||
MANAGE_ALL_CONVERSATION_PERMISSIONS,
|
||||
CONVERSATION_UNASSIGNED_PERMISSIONS,
|
||||
CONVERSATION_PARTICIPATING_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'add',
|
||||
validator: value => ['add', 'edit'].includes(value),
|
||||
},
|
||||
selectedRole: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
const selectedPermissions = ref([]);
|
||||
|
||||
const nameInput = ref(null);
|
||||
|
||||
const addCustomRole = reactive({
|
||||
showLoading: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const rules = computed(() => ({
|
||||
name: { required, minLength: minLength(2) },
|
||||
description: { required },
|
||||
selectedPermissions: { required, minLength: minLength(1) },
|
||||
}));
|
||||
|
||||
const v$ = useVuelidate(rules, { name, description, selectedPermissions });
|
||||
|
||||
const resetForm = () => {
|
||||
name.value = '';
|
||||
description.value = '';
|
||||
selectedPermissions.value = [];
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const populateEditForm = () => {
|
||||
name.value = props.selectedRole.name || '';
|
||||
description.value = props.selectedRole.description || '';
|
||||
selectedPermissions.value = props.selectedRole.permissions || [];
|
||||
};
|
||||
|
||||
watch(
|
||||
selectedPermissions,
|
||||
(newValue, oldValue) => {
|
||||
// Check if manage all conversation permission is added or removed
|
||||
const hasAddedManageAllConversation =
|
||||
newValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS) &&
|
||||
!oldValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS);
|
||||
const hasRemovedManageAllConversation =
|
||||
oldValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS) &&
|
||||
!newValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS);
|
||||
|
||||
if (hasAddedManageAllConversation) {
|
||||
// If manage all conversation permission is added,
|
||||
// then add unassigned and participating permissions automatically
|
||||
selectedPermissions.value = [
|
||||
...new Set([
|
||||
...selectedPermissions.value,
|
||||
CONVERSATION_UNASSIGNED_PERMISSIONS,
|
||||
CONVERSATION_PARTICIPATING_PERMISSIONS,
|
||||
]),
|
||||
];
|
||||
} else if (hasRemovedManageAllConversation) {
|
||||
// If manage all conversation permission is removed,
|
||||
// then only remove manage all conversation permission
|
||||
selectedPermissions.value = selectedPermissions.value.filter(
|
||||
p => p !== MANAGE_ALL_CONVERSATION_PERMISSIONS
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.mode === 'edit') {
|
||||
populateEditForm();
|
||||
}
|
||||
// Focus the name input when mounted
|
||||
nameInput.value?.focus();
|
||||
});
|
||||
|
||||
const getTranslationKey = base => {
|
||||
return props.mode === 'edit'
|
||||
? `CUSTOM_ROLE.EDIT.${base}`
|
||||
: `CUSTOM_ROLE.ADD.${base}`;
|
||||
};
|
||||
|
||||
const modalTitle = computed(() => t(getTranslationKey('TITLE')));
|
||||
const modalDescription = computed(() => t(getTranslationKey('DESC')));
|
||||
const submitButtonText = computed(() => t(getTranslationKey('SUBMIT')));
|
||||
|
||||
const handleCustomRole = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
addCustomRole.showLoading = true;
|
||||
try {
|
||||
const roleData = {
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
permissions: selectedPermissions.value,
|
||||
};
|
||||
|
||||
if (props.mode === 'edit') {
|
||||
await store.dispatch('customRole/updateCustomRole', {
|
||||
id: props.selectedRole.id,
|
||||
...roleData,
|
||||
});
|
||||
useAlert(t('CUSTOM_ROLE.EDIT.API.SUCCESS_MESSAGE'));
|
||||
} else {
|
||||
await store.dispatch('customRole/createCustomRole', roleData);
|
||||
useAlert(t('CUSTOM_ROLE.ADD.API.SUCCESS_MESSAGE'));
|
||||
}
|
||||
|
||||
resetForm();
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t(`CUSTOM_ROLE.FORM.API.ERROR_MESSAGE`);
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
addCustomRole.showLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const isSubmitDisabled = computed(
|
||||
() => v$.value.$invalid || addCustomRole.showLoading
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="modalTitle"
|
||||
:header-content="modalDescription"
|
||||
/>
|
||||
<form class="flex flex-col w-full" @submit.prevent="handleCustomRole">
|
||||
<div class="w-full">
|
||||
<label :class="{ 'text-red-500': v$.name.$error }">
|
||||
{{ $t('CUSTOM_ROLE.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
:class="{ '!border-red-500': v$.name.$error }"
|
||||
:placeholder="$t('CUSTOM_ROLE.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ 'text-red-500': v$.description.$error }">
|
||||
{{ $t('CUSTOM_ROLE.FORM.DESCRIPTION.LABEL') }}
|
||||
</label>
|
||||
<div class="editor-wrap">
|
||||
<WootMessageEditor
|
||||
v-model="description"
|
||||
class="message-editor [&>div]:px-1 h-28"
|
||||
:class="{ editor_warning: v$.description.$error }"
|
||||
enable-variables
|
||||
:focus-on-mount="false"
|
||||
:enable-canned-responses="false"
|
||||
:placeholder="$t('CUSTOM_ROLE.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
@blur="v$.description.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ 'text-red-500': v$.selectedPermissions.$error }">
|
||||
{{ $t('CUSTOM_ROLE.FORM.PERMISSIONS.LABEL') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2.5 mb-4">
|
||||
<div
|
||||
v-for="permission in AVAILABLE_CUSTOM_ROLE_PERMISSIONS"
|
||||
:key="permission"
|
||||
class="flex items-center"
|
||||
>
|
||||
<input
|
||||
:id="permission"
|
||||
v-model="selectedPermissions"
|
||||
type="checkbox"
|
||||
:value="permission"
|
||||
name="permissions"
|
||||
class="ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
<label :for="permission" class="text-sm">
|
||||
{{ $t(`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<WootSubmitButton
|
||||
:disabled="isSubmitDisabled"
|
||||
:button-text="submitButtonText"
|
||||
:loading="addCustomRole.showLoading"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('CUSTOM_ROLE.FORM.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep {
|
||||
.ProseMirror-menubar {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.ProseMirror-woot-style {
|
||||
@apply max-h-[110px];
|
||||
|
||||
p {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRouter } from 'dashboard/composables/route';
|
||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||
import CustomRoleListItem from './CustomRoleTableBody.vue';
|
||||
|
||||
const dummyCustomRolesData = [
|
||||
{
|
||||
name: 'All Permissions',
|
||||
description: 'All permissions',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_participating_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Conversation Permissions',
|
||||
description: 'Conversation permissions',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_participating_manage',
|
||||
'conversation_unassigned_manage',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Contact Permissions',
|
||||
description: 'Contact permissions',
|
||||
permissions: ['contact_manage'],
|
||||
},
|
||||
{
|
||||
name: 'Report Permissions',
|
||||
description: 'Report permissions',
|
||||
permissions: ['report_manage'],
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
const isSuperAdmin = computed(() => {
|
||||
return currentUser.value.type === 'SuperAdmin';
|
||||
});
|
||||
const i18nKey = computed(() =>
|
||||
isOnChatwootCloud.value ? 'PAYWALL' : 'ENTERPRISE_PAYWALL'
|
||||
);
|
||||
|
||||
const goToBillingSettings = () => {
|
||||
router.push({
|
||||
name: 'billing_settings_index',
|
||||
params: { accountId: currentAccountId.value },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full min-h-[12rem] relative">
|
||||
<div class="w-full space-y-3 text-sm">
|
||||
<thead class="opacity-30 dark:opacity-30">
|
||||
<th
|
||||
v-for="thHeader in $t('CUSTOM_ROLE.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
class="py-4 pr-4 font-semibold text-left text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ thHeader }}
|
||||
</span>
|
||||
</th>
|
||||
</thead>
|
||||
<CustomRoleListItem
|
||||
class="opacity-25 dark:opacity-20"
|
||||
:roles="dummyCustomRolesData"
|
||||
:loading="{}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-white dark:from-slate-900 to-transparent"
|
||||
>
|
||||
<BasePaywallModal
|
||||
feature-prefix="CUSTOM_ROLE"
|
||||
:i18n-key="i18nKey"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
:is-super-admin="isSuperAdmin"
|
||||
@click="goToBillingSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
|
||||
defineProps({
|
||||
roles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const getFormattedPermissions = role => {
|
||||
return role.permissions
|
||||
.map(event => t(getI18nKey('CUSTOM_ROLE.PERMISSIONS', event)))
|
||||
.join(', ');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr v-for="(customRole, index) in roles" :key="index">
|
||||
<td
|
||||
class="max-w-xs py-4 pr-4 font-medium truncate align-baseline"
|
||||
:title="customRole.name"
|
||||
>
|
||||
{{ customRole.name }}
|
||||
</td>
|
||||
<td class="py-4 pr-4 whitespace-normal align-baseline md:break-words">
|
||||
{{ customRole.description }}
|
||||
</td>
|
||||
<td class="py-4 pr-4 whitespace-normal align-baseline md:break-words">
|
||||
{{ getFormattedPermissions(customRole) }}
|
||||
</td>
|
||||
<td class="flex justify-end gap-1 py-4">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.EDIT.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="emit('edit', customRole)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[customRole.id]"
|
||||
@click="emit('delete', customRole)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const CustomRolesHome = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/custom-roles'),
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'custom_roles_list',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: CustomRolesHome,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const getI18nKey = (prefix, event) => {
|
||||
const eventName = event.toUpperCase();
|
||||
return `${prefix}.${eventName}`;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getI18nKey } from '../settingsHelper';
|
||||
|
||||
describe('settingsHelper', () => {
|
||||
describe('getI18nKey', () => {
|
||||
it('should return the correct i18n key', () => {
|
||||
const prefix = 'CUSTOM_ROLE.PERMISSIONS';
|
||||
const event = 'conversation_manage';
|
||||
const expectedKey = 'CUSTOM_ROLE.PERMISSIONS.CONVERSATION_MANAGE';
|
||||
|
||||
expect(getI18nKey(prefix, event)).toBe(expectedKey);
|
||||
});
|
||||
|
||||
it('should handle different prefixes', () => {
|
||||
const prefix = 'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS';
|
||||
const event = 'message_created';
|
||||
const expectedKey =
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED';
|
||||
|
||||
expect(getI18nKey(prefix, event)).toBe(expectedKey);
|
||||
});
|
||||
|
||||
it('should convert event to uppercase', () => {
|
||||
const prefix = 'TEST_PREFIX';
|
||||
const event = 'lowercaseEvent';
|
||||
const expectedKey = 'TEST_PREFIX.LOWERCASEEVENT';
|
||||
|
||||
expect(getI18nKey(prefix, event)).toBe(expectedKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, url, minLength } from '@vuelidate/validators';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { getEventNamei18n } from './webhookHelper';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
|
||||
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
getEventNamei18n,
|
||||
getI18nKey,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -108,13 +108,20 @@ export default {
|
||||
class="mr-2"
|
||||
/>
|
||||
<label :for="event" class="text-sm">
|
||||
{{ `${$t(getEventNamei18n(event))} (${event})` }}
|
||||
{{
|
||||
`${$t(
|
||||
getI18nKey(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS',
|
||||
event
|
||||
)
|
||||
)} (${event})`
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-button
|
||||
:disabled="v$.$invalid || isSubmitting"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { getEventNamei18n } from './webhookHelper';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
@@ -17,7 +17,16 @@ const props = defineProps({
|
||||
const { t } = useI18n();
|
||||
const subscribedEvents = computed(() => {
|
||||
const { subscriptions } = props.webhook;
|
||||
return subscriptions.map(event => t(getEventNamei18n(event))).join(', ');
|
||||
return subscriptions
|
||||
.map(event =>
|
||||
t(
|
||||
getI18nKey(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS',
|
||||
event
|
||||
)
|
||||
)
|
||||
)
|
||||
.join(', ');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -27,7 +36,7 @@ const subscribedEvents = computed(() => {
|
||||
<div class="font-medium break-words text-slate-700 dark:text-slate-100">
|
||||
{{ webhook.url }}
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400 block mt-1">
|
||||
<div class="block mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
<span class="font-medium">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
|
||||
</span>
|
||||
@@ -35,7 +44,7 @@ const subscribedEvents = computed(() => {
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<div class="flex justify-end gap-1">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { getEventNamei18n } from '../webhookHelper';
|
||||
|
||||
describe('#getEventNamei18n', () => {
|
||||
it('returns correct i18n translation text', () => {
|
||||
expect(getEventNamei18n('message_created')).toEqual(
|
||||
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export const getEventNamei18n = event => {
|
||||
const eventName = event.toUpperCase();
|
||||
return `INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}`;
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const Macros = () => import('./Index.vue');
|
||||
@@ -16,7 +20,7 @@ export default {
|
||||
name: 'macros_wrapper',
|
||||
component: Macros,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -37,7 +41,7 @@ export default {
|
||||
name: 'macros_edit',
|
||||
component: MacroEditor,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -45,7 +49,7 @@ export default {
|
||||
name: 'macros_new',
|
||||
component: MacroEditor,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,12 +14,18 @@ import NotificationPreferences from './NotificationPreferences.vue';
|
||||
import AudioNotifications from './AudioNotifications.vue';
|
||||
import FormSection from 'dashboard/components/FormSection.vue';
|
||||
import AccessToken from './AccessToken.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageSignature,
|
||||
FormSection,
|
||||
UserProfilePicture,
|
||||
Policy,
|
||||
UserBasicDetails,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
@@ -71,6 +77,8 @@ export default {
|
||||
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
|
||||
},
|
||||
],
|
||||
notificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
audioNotificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -235,6 +243,7 @@ export default {
|
||||
>
|
||||
<ChangePassword />
|
||||
</FormSection>
|
||||
<Policy :permissions="audioNotificationPermissions">
|
||||
<FormSection
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
@@ -243,9 +252,12 @@ export default {
|
||||
>
|
||||
<AudioNotifications />
|
||||
</FormSection>
|
||||
</Policy>
|
||||
<Policy :permissions="notificationPermissions">
|
||||
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<NotificationPreferences />
|
||||
</FormSection>
|
||||
</Policy>
|
||||
<FormSection
|
||||
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
||||
:description="
|
||||
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/profile'),
|
||||
name: 'profile_settings',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
children: [
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
name: 'profile_settings_index',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
path: 'overview',
|
||||
name: 'account_overview_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LiveReports,
|
||||
},
|
||||
@@ -49,7 +49,7 @@ export default {
|
||||
path: 'conversation',
|
||||
name: 'conversation_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
path: 'csat',
|
||||
name: 'csat_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: CsatResponses,
|
||||
},
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
path: 'bot',
|
||||
name: 'bot_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: BotReports,
|
||||
},
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
path: 'agent',
|
||||
name: 'agent_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: AgentReports,
|
||||
},
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
path: 'label',
|
||||
name: 'label_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LabelReports,
|
||||
},
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
path: 'inboxes',
|
||||
name: 'inbox_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: InboxReports,
|
||||
},
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
path: 'teams',
|
||||
name: 'team_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: TeamReports,
|
||||
},
|
||||
@@ -181,7 +181,7 @@ export default {
|
||||
path: 'sla',
|
||||
name: 'sla_reports',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: SLAReports,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
import account from './account/account.routes';
|
||||
import agent from './agents/agent.routes';
|
||||
import agentBot from './agentBots/agentBot.routes';
|
||||
@@ -16,6 +21,7 @@ import reports from './reports/reports.routes';
|
||||
import store from '../../../store';
|
||||
import sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import customRoles from './customRoles/customRole.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
|
||||
export default {
|
||||
@@ -24,10 +30,13 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/settings'),
|
||||
name: 'settings_home',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
redirect: () => {
|
||||
if (store.getters.getCurrentRole === 'administrator') {
|
||||
if (
|
||||
store.getters.getCurrentRole === 'administrator' &&
|
||||
store.getters.getCurrentCustomRoleId === null
|
||||
) {
|
||||
return frontendURL('accounts/:accountId/settings/general');
|
||||
}
|
||||
return frontendURL('accounts/:accountId/settings/canned-response');
|
||||
@@ -49,6 +58,7 @@ export default {
|
||||
...reports.routes,
|
||||
...sla.routes,
|
||||
...teams.routes,
|
||||
...customRoles.routes,
|
||||
...profile.routes,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import BaseEmptyState from './BaseEmptyState.vue';
|
||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
isSuperAdmin: {
|
||||
@@ -18,59 +19,12 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
|
||||
|
||||
<template>
|
||||
<BaseEmptyState>
|
||||
<div
|
||||
class="flex flex-col max-w-md px-6 py-6 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-100 dark:border-slate-900"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2 mb-4">
|
||||
<span
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full bg-woot-75/70 dark:bg-woot-800/40"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base font-medium text-slate-900 dark:text-white">
|
||||
{{ $t('SLA.PAYWALL.TITLE') }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-normal"
|
||||
v-html="$t(`SLA.${i18nKey}.AVAILABLE_ON`)"
|
||||
/>
|
||||
<p class="text-sm font-normal">
|
||||
{{ $t(`SLA.${i18nKey}.UPGRADE_PROMPT`) }}
|
||||
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
|
||||
{{ $t('SLA.ENTERPRISE_PAYWALL.ASK_ADMIN') }}
|
||||
</span>
|
||||
</p>
|
||||
<template v-if="isOnChatwootCloud || true">
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
class="w-full mt-2 text-center rounded-xl"
|
||||
size="expanded"
|
||||
is-expanded
|
||||
<BasePaywallModal
|
||||
feature-prefix="SLA"
|
||||
:i18n-key="i18nKey"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
:is-super-admin="isSuperAdmin"
|
||||
@click="emit('click')"
|
||||
>
|
||||
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
|
||||
</woot-button>
|
||||
<span class="mt-2 text-xs tracking-tight text-center">
|
||||
{{ $t('SLA.PAYWALL.CANCEL_ANYTIME') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="isSuperAdmin">
|
||||
<a href="/super_admin" class="block w-full">
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
class="w-full mt-2 text-center rounded-xl"
|
||||
size="expanded"
|
||||
is-expanded
|
||||
>
|
||||
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
/>
|
||||
</BaseEmptyState>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +27,7 @@ import conversationTypingStatus from './modules/conversationTypingStatus';
|
||||
import conversationWatchers from './modules/conversationWatchers';
|
||||
import csat from './modules/csat';
|
||||
import customViews from './modules/customViews';
|
||||
import customRole from './modules/customRole';
|
||||
import dashboardApps from './modules/dashboardApps';
|
||||
import globalConfig from 'shared/store/globalConfig';
|
||||
import inboxAssignableAgents from './modules/inboxAssignableAgents';
|
||||
@@ -77,6 +78,7 @@ export default new Vuex.Store({
|
||||
conversationWatchers,
|
||||
csat,
|
||||
customViews,
|
||||
customRole,
|
||||
dashboardApps,
|
||||
globalConfig,
|
||||
inboxAssignableAgents,
|
||||
|
||||
@@ -66,6 +66,14 @@ export const getters = {
|
||||
return currentAccount.role;
|
||||
},
|
||||
|
||||
getCurrentCustomRoleId($state, $getters) {
|
||||
const { accounts = [] } = $state.currentUser;
|
||||
const [currentAccount = {}] = accounts.filter(
|
||||
account => account.id === $getters.getCurrentAccountId
|
||||
);
|
||||
return currentAccount.custom_role_id;
|
||||
},
|
||||
|
||||
getCurrentUser($state) {
|
||||
return $state.currentUser;
|
||||
},
|
||||
|
||||
100
app/javascript/dashboard/store/modules/customRole.js
Normal file
100
app/javascript/dashboard/store/modules/customRole.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import CustomRoleAPI from '../../api/customRole';
|
||||
|
||||
export const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
fetchingList: false,
|
||||
creatingItem: false,
|
||||
updatingItem: false,
|
||||
deletingItem: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getCustomRoles($state) {
|
||||
return $state.records;
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
getCustomRole: async function getCustomRole({ commit }) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true });
|
||||
try {
|
||||
const response = await CustomRoleAPI.get();
|
||||
commit(types.default.SET_CUSTOM_ROLE, response.data);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
|
||||
}
|
||||
},
|
||||
|
||||
createCustomRole: async function createCustomRole({ commit }, customRoleObj) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true });
|
||||
try {
|
||||
const response = await CustomRoleAPI.create(customRoleObj);
|
||||
commit(types.default.ADD_CUSTOM_ROLE, response.data);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
updateCustomRole: async function updateCustomRole(
|
||||
{ commit },
|
||||
{ id, ...updateObj }
|
||||
) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true });
|
||||
try {
|
||||
const response = await CustomRoleAPI.update(id, updateObj);
|
||||
commit(types.default.EDIT_CUSTOM_ROLE, response.data);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
deleteCustomRole: async function deleteCustomRole({ commit }, id) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
|
||||
try {
|
||||
await CustomRoleAPI.delete(id);
|
||||
commit(types.default.DELETE_CUSTOM_ROLE, id);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
|
||||
return id;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG](_state, data) {
|
||||
_state.uiFlags = {
|
||||
..._state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
[types.default.SET_CUSTOM_ROLE]: MutationHelpers.set,
|
||||
[types.default.ADD_CUSTOM_ROLE]: MutationHelpers.create,
|
||||
[types.default.EDIT_CUSTOM_ROLE]: MutationHelpers.update,
|
||||
[types.default.DELETE_CUSTOM_ROLE]: MutationHelpers.destroy,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -42,6 +42,26 @@ describe('#getters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCurrentCustomRoleId', () => {
|
||||
it('returns current custom role id', () => {
|
||||
expect(
|
||||
getters.getCurrentCustomRoleId(
|
||||
{ currentUser: { accounts: [{ id: 1, custom_role_id: 1 }] } },
|
||||
{ getCurrentAccountId: 1 }
|
||||
)
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('returns undefined if account is not available', () => {
|
||||
expect(
|
||||
getters.getCurrentCustomRoleId(
|
||||
{ currentUser: { accounts: [{ id: 1, custom_role_id: 1 }] } },
|
||||
{ getCurrentAccountId: 2 }
|
||||
)
|
||||
).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCurrentUserAvailability', () => {
|
||||
it('returns correct availability status', () => {
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../customRole';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { customRoleList } from './fixtures';
|
||||
|
||||
const commit = vi.fn();
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#getCustomRole', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: customRoleList });
|
||||
await actions.getCustomRole({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true }],
|
||||
[types.default.SET_CUSTOM_ROLE, customRoleList],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.getCustomRole({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createCustomRole', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: customRoleList[0] });
|
||||
await actions.createCustomRole({ commit }, customRoleList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true }],
|
||||
[types.default.ADD_CUSTOM_ROLE, customRoleList[0]],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.createCustomRole({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateCustomRole', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: customRoleList[0] });
|
||||
await actions.updateCustomRole(
|
||||
{ commit },
|
||||
{ id: 1, ...customRoleList[0] }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true }],
|
||||
[types.default.EDIT_CUSTOM_ROLE, customRoleList[0]],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.updateCustomRole({ commit }, { id: 1 })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteCustomRole', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({ data: customRoleList[0] });
|
||||
await actions.deleteCustomRole({ commit }, 1);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
[types.default.DELETE_CUSTOM_ROLE, 1],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.deleteCustomRole({ commit }, 1)).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
export const customRoleList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Custom Role',
|
||||
description: 'Role with all available custom role permissions',
|
||||
permissions: [
|
||||
'conversation_participating_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
created_at: '2024-09-04T05:30:22.282Z',
|
||||
updated_at: '2024-09-05T09:21:02.844Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Conversation Manager Role',
|
||||
description: 'Role for managing all aspects of conversations',
|
||||
permissions: [
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'conversation_manage',
|
||||
],
|
||||
created_at: '2024-09-05T09:21:38.692Z',
|
||||
updated_at: '2024-09-05T09:21:38.692Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Participating Agent Role',
|
||||
description: 'Role for agents participating in conversations',
|
||||
permissions: ['conversation_participating_manage'],
|
||||
created_at: '2024-09-06T08:03:14.550Z',
|
||||
updated_at: '2024-09-06T08:03:14.550Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Contact Manager Role',
|
||||
description: 'Role for managing contacts only',
|
||||
permissions: ['contact_manage'],
|
||||
created_at: '2024-09-06T08:15:56.877Z',
|
||||
updated_at: '2024-09-06T09:53:28.103Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Report Analyst Role',
|
||||
description: 'Role for accessing and managing reports',
|
||||
permissions: ['report_manage'],
|
||||
created_at: '2024-09-06T09:53:58.277Z',
|
||||
updated_at: '2024-09-06T09:53:58.277Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Knowledge Base Editor Role',
|
||||
description: 'Role for managing the knowledge base',
|
||||
permissions: ['knowledge_base_manage'],
|
||||
created_at: '2024-09-06T09:54:27.649Z',
|
||||
updated_at: '2024-09-06T09:54:27.649Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Unassigned Queue Manager Role',
|
||||
description: 'Role for managing unassigned conversations',
|
||||
permissions: ['conversation_unassigned_manage'],
|
||||
created_at: '2024-09-06T09:55:00.503Z',
|
||||
updated_at: '2024-09-06T09:55:00.503Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Basic Conversation Handler Role',
|
||||
description: 'Role for basic conversation management',
|
||||
permissions: ['conversation_manage'],
|
||||
created_at: '2024-09-06T09:55:19.519Z',
|
||||
updated_at: '2024-09-06T09:55:19.519Z',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getters } from '../../customRole';
|
||||
import { customRoleList } from './fixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getCustomRoles', () => {
|
||||
const state = { records: customRoleList };
|
||||
expect(getters.getCustomRoles(state)).toEqual(customRoleList);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
fetchingList: true,
|
||||
creatingItem: false,
|
||||
updatingItem: false,
|
||||
deletingItem: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
fetchingList: true,
|
||||
creatingItem: false,
|
||||
updatingItem: false,
|
||||
deletingItem: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../customRole';
|
||||
import { customRoleList } from './fixtures';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CUSTOM_ROLE', () => {
|
||||
it('set custom role records', () => {
|
||||
const state = { records: [] };
|
||||
mutations[types.SET_CUSTOM_ROLE](state, customRoleList);
|
||||
expect(state.records).toEqual(customRoleList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_CUSTOM_ROLE', () => {
|
||||
it('push newly created custom role to the store', () => {
|
||||
const state = { records: [customRoleList[0]] };
|
||||
mutations[types.ADD_CUSTOM_ROLE](state, customRoleList[1]);
|
||||
expect(state.records).toEqual([customRoleList[0], customRoleList[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#EDIT_CUSTOM_ROLE', () => {
|
||||
it('update custom role record', () => {
|
||||
const state = { records: [customRoleList[0]] };
|
||||
const updatedRole = { ...customRoleList[0], name: 'Updated Role' };
|
||||
mutations[types.EDIT_CUSTOM_ROLE](state, updatedRole);
|
||||
expect(state.records).toEqual([updatedRole]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_CUSTOM_ROLE', () => {
|
||||
it('delete custom role record', () => {
|
||||
const state = { records: [customRoleList[0], customRoleList[1]] };
|
||||
mutations[types.DELETE_CUSTOM_ROLE](state, customRoleList[0].id);
|
||||
expect(state.records).toEqual([customRoleList[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CUSTOM_ROLE_UI_FLAG', () => {
|
||||
it('set custom role UI flags', () => {
|
||||
const state = { uiFlags: {} };
|
||||
mutations[types.SET_CUSTOM_ROLE_UI_FLAG](state, {
|
||||
fetchingList: true,
|
||||
});
|
||||
expect(state.uiFlags).toEqual({ fetchingList: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -98,6 +98,13 @@ export default {
|
||||
EDIT_CANNED: 'EDIT_CANNED',
|
||||
DELETE_CANNED: 'DELETE_CANNED',
|
||||
|
||||
// Custom Role
|
||||
SET_CUSTOM_ROLE_UI_FLAG: 'SET_CUSTOM_ROLE_UI_FLAG',
|
||||
SET_CUSTOM_ROLE: 'SET_CUSTOM_ROLE',
|
||||
ADD_CUSTOM_ROLE: 'ADD_CUSTOM_ROLE',
|
||||
EDIT_CUSTOM_ROLE: 'EDIT_CUSTOM_ROLE',
|
||||
DELETE_CUSTOM_ROLE: 'DELETE_CUSTOM_ROLE',
|
||||
|
||||
// Labels
|
||||
SET_LABEL_UI_FLAG: 'SET_LABEL_UI_FLAG',
|
||||
SET_LABELS: 'SET_LABELS',
|
||||
|
||||
@@ -271,15 +271,9 @@
|
||||
"M11.1667 17.8333C14.8486 17.8333 17.8333 14.8486 17.8333 11.1667C17.8333 7.48477 14.8486 4.5 11.1667 4.5C7.48477 4.5 4.5 7.48477 4.5 11.1667C4.5 14.8486 7.48477 17.8333 11.1667 17.8333Z",
|
||||
"M19.5001 19.5001L15.9167 15.9167"
|
||||
],
|
||||
"chevrons-left-outline": [
|
||||
"m11 17-5-5 5-5",
|
||||
"m18 17-5-5 5-5"
|
||||
],
|
||||
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
|
||||
"chevron-left-single-outline": "m15 18-6-6 6-6",
|
||||
"chevrons-right-outline": [
|
||||
"m6 17 5-5-5-5",
|
||||
"m13 17 5-5-5-5"
|
||||
],
|
||||
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
|
||||
"chevron-right-single-outline": "m9 18 6-6-6-6",
|
||||
"avatar-upload-outline": "M19.754 11a.75.75 0 0 1 .743.648l.007.102v7a3.25 3.25 0 0 1-3.065 3.246l-.185.005h-11a3.25 3.25 0 0 1-3.244-3.066l-.006-.184V11.75a.75.75 0 0 1 1.494-.102l.006.102v7a1.75 1.75 0 0 0 1.607 1.745l.143.006h11A1.75 1.75 0 0 0 19 18.894l.005-.143V11.75a.75.75 0 0 1 .75-.75ZM6.22 7.216l4.996-4.996a.75.75 0 0 1 .976-.073l.084.072l5.005 4.997a.75.75 0 0 1-.976 1.134l-.084-.073l-3.723-3.716l.001 11.694a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L11 16.255V4.558L7.28 8.277a.75.75 0 0 1-.976.073l-.084-.073a.75.75 0 0 1-.073-.977l.073-.084l4.996-4.996L6.22 7.216Z",
|
||||
"text-copy-outline": "M5.503 4.627L5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.25 2.25 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123M17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75",
|
||||
@@ -290,5 +284,6 @@
|
||||
"M6.49573 9.20645C6.49573 8.67008 6.93058 8.23523 7.46695 8.23523C8.00337 8.23523 8.43817 8.67008 8.43817 9.20645V11.4511C8.43817 11.9875 8.00337 12.4223 7.46695 12.4223C6.93058 12.4223 6.49573 11.9875 6.49573 11.4511V9.20645Z",
|
||||
"M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z",
|
||||
"M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z"
|
||||
]
|
||||
],
|
||||
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0"
|
||||
}
|
||||
|
||||
@@ -33,3 +33,5 @@ class ArticlePolicy < ApplicationPolicy
|
||||
@record.first.portal.members.include?(@user)
|
||||
end
|
||||
end
|
||||
|
||||
ArticlePolicy.prepend_mod_with('Enterprise::ArticlePolicy')
|
||||
|
||||
@@ -29,3 +29,5 @@ class CategoryPolicy < ApplicationPolicy
|
||||
@record.first.portal.members.include?(@user)
|
||||
end
|
||||
end
|
||||
|
||||
CategoryPolicy.prepend_mod_with('Enterprise::CategoryPolicy')
|
||||
|
||||
@@ -37,3 +37,5 @@ class PortalPolicy < ApplicationPolicy
|
||||
@record.first.members.include?(@user)
|
||||
end
|
||||
end
|
||||
|
||||
PortalPolicy.prepend_mod_with('Enterprise::PortalPolicy')
|
||||
|
||||
@@ -3,3 +3,5 @@ class ReportPolicy < ApplicationPolicy
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
ReportPolicy.prepend_mod_with('Enterprise::ReportPolicy')
|
||||
|
||||
@@ -30,5 +30,6 @@ json.accounts do
|
||||
# availability derived from presence
|
||||
json.availability_status account_user.availability_status
|
||||
json.auto_offline account_user.auto_offline
|
||||
json.partial! 'api/v1/models/account_user', account_user: account_user if ChatwootApp.enterprise?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,3 +89,6 @@
|
||||
enabled: false
|
||||
- name: captain_integration
|
||||
enabled: false
|
||||
- name: custom_roles
|
||||
enabled: false
|
||||
premium: true
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
module Enterprise::Api::V1::Accounts::AgentsController
|
||||
def account_user_attributes
|
||||
super + [:custom_role_id]
|
||||
def create
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
def allowed_agent_params
|
||||
super + [:custom_role_id]
|
||||
def update
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def associate_agent_with_custom_role
|
||||
@agent.current_account_user.update!(custom_role_id: params[:custom_role_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Enterprise::Api::V2::Accounts::ReportsController
|
||||
def check_authorization
|
||||
return if Current.account_user.custom_role&.permissions&.include?('report_manage')
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
29
enterprise/app/policies/enterprise/article_policy.rb
Normal file
29
enterprise/app/policies/enterprise/article_policy.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module Enterprise::ArticlePolicy
|
||||
def index?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def edit?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def reorder?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
end
|
||||
25
enterprise/app/policies/enterprise/category_policy.rb
Normal file
25
enterprise/app/policies/enterprise/category_policy.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module Enterprise::CategoryPolicy
|
||||
def index?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def edit?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
end
|
||||
33
enterprise/app/policies/enterprise/portal_policy.rb
Normal file
33
enterprise/app/policies/enterprise/portal_policy.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
module Enterprise::PortalPolicy
|
||||
def index?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def edit?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def add_members?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
|
||||
def logo?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
|
||||
end
|
||||
end
|
||||
5
enterprise/app/policies/enterprise/report_policy.rb
Normal file
5
enterprise/app/policies/enterprise/report_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Enterprise::ReportPolicy
|
||||
def view?
|
||||
@account_user.custom_role&.permissions&.include?('report_manage') || super
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
json.custom_role_id account_user&.custom_role_id
|
||||
json.custom_role account_user&.custom_role&.as_json(only: [:id, :name, :description, :permissions])
|
||||
@@ -0,0 +1,28 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Profile API', type: :request do
|
||||
describe 'GET /api/v1/profile' do
|
||||
let(:account) { create(:account) }
|
||||
let!(:custom_role_account) { create(:account, name: 'Custom Role Account') }
|
||||
let!(:custom_role) { create(:custom_role, name: 'Custom Role', account: custom_role_account) }
|
||||
let!(:agent) { create(:user, account: account, custom_attributes: { test: 'test' }, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:account_user, account: custom_role_account, user: agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'returns user custom role information' do
|
||||
get '/api/v1/profile',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
parsed_response = response.parsed_body
|
||||
# map accounts object and make sure custom role id and name are present
|
||||
role_account = parsed_response['accounts'].find { |account| account['id'] == custom_role_account.id }
|
||||
expect(role_account['custom_role']['id']).to eq(custom_role.id)
|
||||
expect(role_account['custom_role']['name']).to eq(custom_role.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,10 +3,25 @@ require 'rails_helper'
|
||||
RSpec.describe 'Enterprise Agents API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let!(:custom_role) { create(:custom_role, account: account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/agents' do
|
||||
let(:params) { { email: 'test@example.com', name: 'Test User', role: 'agent', custom_role_id: custom_role.id } }
|
||||
|
||||
context 'when it is an authenticated administrator' do
|
||||
it 'creates an agent with the specified custom role' do
|
||||
post "/api/v1/accounts/#{account.id}/agents", headers: admin.create_new_auth_token, params: params, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
agent = account.agents.last
|
||||
expect(agent.account_users.first.custom_role_id).to eq(custom_role.id)
|
||||
expect(JSON.parse(response.body)['custom_role_id']).to eq(custom_role.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}/agents/:id' do
|
||||
let(:other_agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:custom_role) { create(:custom_role, account: account) }
|
||||
|
||||
context 'when it is an authenticated administrator' do
|
||||
it 'modified the custom role of the agent' do
|
||||
|
||||
Reference in New Issue
Block a user