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:
@@ -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,156 +1,164 @@
|
||||
<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: () => {},
|
||||
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 },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, {
|
||||
agentName,
|
||||
agentEmail,
|
||||
selectedRoleId,
|
||||
});
|
||||
|
||||
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'),
|
||||
},
|
||||
},
|
||||
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,
|
||||
{
|
||||
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,
|
||||
};
|
||||
},
|
||||
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,
|
||||
});
|
||||
useAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const {
|
||||
response: {
|
||||
data: {
|
||||
error: errorResponse = '',
|
||||
attributes: attributes = [],
|
||||
message: attrError = '',
|
||||
} = {},
|
||||
} = {},
|
||||
} = error;
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
}
|
||||
|
||||
let errorMessage = '';
|
||||
if (error?.response?.status === 422 && !attributes.includes('base')) {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
} else {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
}
|
||||
useAlert(errorResponse || attrError || errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
await store.dispatch('agents/create', payload);
|
||||
useAlert(t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
const {
|
||||
response: {
|
||||
data: {
|
||||
error: errorResponse = '',
|
||||
attributes: attributes = [],
|
||||
message: attrError = '',
|
||||
} = {},
|
||||
} = {},
|
||||
} = error;
|
||||
|
||||
let errorMessage = '';
|
||||
if (error?.response?.status === 422 && !attributes.includes('base')) {
|
||||
errorMessage = t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
} else {
|
||||
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')"
|
||||
/>
|
||||
<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">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="agentName"
|
||||
type="text"
|
||||
:placeholder="$t('AGENT_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.agentName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="flex flex-col items-start w-full"
|
||||
@submit.prevent="addAgent()"
|
||||
>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.selectedRoleId.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.LABEL') }}
|
||||
<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$.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="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">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="agentName"
|
||||
type="text"
|
||||
:placeholder="$t('AGENT_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.agentName.$touch"
|
||||
/>
|
||||
</label>
|
||||
<WootSubmitButton
|
||||
:disabled="v$.$invalid || uiFlags.isCreating"
|
||||
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('AGENT_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentType.$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">
|
||||
{{ role.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentType.$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"
|
||||
: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
|
||||
"
|
||||
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('AGENT_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,203 +1,220 @@
|
||||
<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 props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
availability: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
customRoleId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
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'),
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'agent',
|
||||
label: t('AGENT_MGMT.AGENT_TYPES.AGENT'),
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
availability: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
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 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: props.availability === AVAILABILITY_STATUS_KEYS[index],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const editAgent = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
id: props.id,
|
||||
name: agentName.value,
|
||||
availability: agentAvailability.value,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
agentName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
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(
|
||||
(statusLabel, index) => ({
|
||||
label: statusLabel,
|
||||
value: AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled:
|
||||
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async editAgent() {
|
||||
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'));
|
||||
}
|
||||
},
|
||||
async resetPassword() {
|
||||
try {
|
||||
await Auth.resetPassword(this.agentCredentials);
|
||||
useAlert(
|
||||
this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
payload.custom_role_id = null;
|
||||
}
|
||||
|
||||
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(agentCredentials.value);
|
||||
useAlert(t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
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()">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="agentName"
|
||||
type="text"
|
||||
:placeholder="$t('AGENT_MGMT.EDIT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.agentName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="pageTitle" />
|
||||
<form class="w-full" @submit.prevent="editAgent">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentName.$error }">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="agentName"
|
||||
type="text"
|
||||
:placeholder="$t('AGENT_MGMT.EDIT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.agentName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentType.$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">
|
||||
{{ role.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentType.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.selectedRoleId.$error }">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.LABEL') }}
|
||||
<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$.selectedRoleId.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
{{ role.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentAvailability.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_AVAILABILITY.ERROR') }}
|
||||
</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
|
||||
"
|
||||
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isUpdating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('AGENT_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-[50%] text-right">
|
||||
<woot-button
|
||||
icon="lock-closed"
|
||||
variant="clear"
|
||||
@click.prevent="resetPassword"
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.agentAvailability.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.AVAILABILITY.LABEL') }}
|
||||
<select
|
||||
v-model="agentAvailability"
|
||||
@change="v$.agentAvailability.$touch"
|
||||
>
|
||||
<option
|
||||
v-for="status in availabilityStatuses"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_RESET_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
{{ status.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.agentAvailability.$error" class="message">
|
||||
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_AVAILABILITY.ERROR') }}
|
||||
</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$.$invalid || uiFlags.isUpdating"
|
||||
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isUpdating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('AGENT_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
<div class="w-[50%] text-right">
|
||||
<woot-button
|
||||
icon="lock-closed"
|
||||
variant="clear"
|
||||
@click.prevent="resetPassword"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_RESET_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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,17 +243,21 @@ export default {
|
||||
>
|
||||
<ChangePassword />
|
||||
</FormSection>
|
||||
<FormSection
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<AudioNotifications />
|
||||
</FormSection>
|
||||
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<NotificationPreferences />
|
||||
</FormSection>
|
||||
<Policy :permissions="audioNotificationPermissions">
|
||||
<FormSection
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<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
|
||||
@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>
|
||||
<BasePaywallModal
|
||||
feature-prefix="SLA"
|
||||
:i18n-key="i18nKey"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
:is-super-admin="isSuperAdmin"
|
||||
@click="emit('click')"
|
||||
/>
|
||||
</BaseEmptyState>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user