feat: complete current GoChat updates
This commit is contained in:
@@ -4,6 +4,14 @@ class CustomRole extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_roles', { accountScoped: true });
|
||||
}
|
||||
|
||||
create(data) {
|
||||
return super.create({ custom_role: data });
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return super.update(id, { custom_role: data });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomRole();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import customRole from '../customRole';
|
||||
|
||||
describe('#CustomRoleAPI', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('wraps create and update payloads for the backend contract', () => {
|
||||
const payload = {
|
||||
name: 'Support',
|
||||
description: 'Support role',
|
||||
permissions: ['contact_manage'],
|
||||
};
|
||||
|
||||
customRole.create(payload);
|
||||
customRole.update(7, payload);
|
||||
|
||||
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/custom_roles', {
|
||||
custom_role: payload,
|
||||
});
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith('/api/v1/custom_roles/7', {
|
||||
custom_role: payload,
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -126,24 +126,10 @@ export function useSidebarContext() {
|
||||
return router.resolve(to)?.meta?.featureFlag || '';
|
||||
};
|
||||
|
||||
const resolveInstallationType = to => {
|
||||
if (!to) return [];
|
||||
|
||||
// If navigationPath param exists, get the target route definition
|
||||
if (to.params?.navigationPath) {
|
||||
const targetRoute = findRouteByName(to.params.navigationPath);
|
||||
return targetRoute?.meta?.installationTypes || [];
|
||||
}
|
||||
|
||||
return router.resolve(to)?.meta?.installationTypes || [];
|
||||
};
|
||||
|
||||
const isAllowed = to => {
|
||||
const permissions = resolvePermissions(to);
|
||||
const featureFlag = resolveFeatureFlag(to);
|
||||
const installationType = resolveInstallationType(to);
|
||||
|
||||
return shouldShow(featureFlag, permissions, installationType);
|
||||
return shouldShow(featureFlag, permissions);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,17 +15,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
installationTypes: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { shouldShow } = usePolicy();
|
||||
|
||||
const show = computed(() =>
|
||||
shouldShow(props.featureFlag, props.permissions, props.installationTypes)
|
||||
);
|
||||
const show = computed(() => shouldShow(props.featureFlag, props.permissions));
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-root-v-if -->
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
} from 'dashboard/helper/permissionsHelper';
|
||||
import { PREMIUM_FEATURES } from 'dashboard/featureFlags';
|
||||
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
|
||||
export function usePolicy() {
|
||||
const user = useMapGetter('getCurrentUser');
|
||||
const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount');
|
||||
@@ -34,35 +32,17 @@ export function usePolicy() {
|
||||
return hasPermissions(requiredPermissions, userPermissions);
|
||||
};
|
||||
|
||||
const checkInstallationType = config => {
|
||||
if (Array.isArray(config) && config.length > 0) {
|
||||
const installationCheck = {
|
||||
[INSTALLATION_TYPES.ENTERPRISE]: true,
|
||||
[INSTALLATION_TYPES.CLOUD]: isOnChatwootCloud.value,
|
||||
[INSTALLATION_TYPES.COMMUNITY]: true,
|
||||
};
|
||||
|
||||
return config.some(type => installationCheck[type]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isPremiumFeature = featureFlag => {
|
||||
if (!featureFlag) return true;
|
||||
return PREMIUM_FEATURES.includes(featureFlag);
|
||||
};
|
||||
|
||||
const shouldShow = (featureFlag, permissions, installationTypes) => {
|
||||
const shouldShow = (featureFlag, permissions) => {
|
||||
const flag = unref(featureFlag);
|
||||
const perms = unref(permissions);
|
||||
const installation = unref(installationTypes);
|
||||
|
||||
// if the user does not have permissions or installation type is not supported
|
||||
// return false;
|
||||
// This supersedes everything
|
||||
// Permissions supersede feature visibility.
|
||||
if (!checkPermissions(perms)) return false;
|
||||
if (!checkInstallationType(installation)) return false;
|
||||
|
||||
if (isACustomBrandedInstance.value) {
|
||||
// if this is a custom branded instance, we just use the feature flag as a reference
|
||||
@@ -76,8 +56,9 @@ export function usePolicy() {
|
||||
return isFeatureFlagEnabled(flag) || isPremiumFeature(flag);
|
||||
}
|
||||
|
||||
// default to true
|
||||
return true;
|
||||
// Premium routes remain visible so their page can render its paywall;
|
||||
// other routes still honor the account feature flag on self-hosted installs.
|
||||
return isFeatureFlagEnabled(flag) || isPremiumFeature(flag);
|
||||
};
|
||||
|
||||
const shouldShowPaywall = featureFlag => {
|
||||
@@ -89,7 +70,7 @@ export function usePolicy() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPremiumFeature(flag) && isOnChatwootCloud.value) {
|
||||
if (isPremiumFeature(flag)) {
|
||||
return !isFeatureFlagEnabled(flag);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export const INSTALLATION_TYPES = {
|
||||
CLOUD: 'cloud',
|
||||
ENTERPRISE: 'enterprise',
|
||||
COMMUNITY: 'community',
|
||||
};
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
} from './permissionsHelper';
|
||||
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
REPORTS_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
import { isSuperAdminUser } from '../routes/dashboard/settings/captain/utils';
|
||||
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
@@ -22,7 +22,7 @@ export const defaultRedirectPage = (to, permissions) => {
|
||||
|
||||
const permissionRoutes = [
|
||||
{
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
permissions: CONVERSATION_PERMISSIONS,
|
||||
path: 'dashboard',
|
||||
},
|
||||
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
|
||||
@@ -34,7 +34,11 @@ export const defaultRedirectPage = (to, permissions) => {
|
||||
hasPermissions(routePermissions, permissions)
|
||||
);
|
||||
|
||||
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
|
||||
if (route) return `accounts/${accountId}/${route.path}`;
|
||||
if (permissions.includes('custom_role')) {
|
||||
return `accounts/${accountId}/profile/settings`;
|
||||
}
|
||||
return `accounts/${accountId}/dashboard`;
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
@@ -48,6 +52,10 @@ const validateActiveAccountRoutes = (to, user) => {
|
||||
|
||||
const userPermissions = getUserPermissions(user, to.params.accountId);
|
||||
|
||||
if (to.meta?.isSuperAdmin && !isSuperAdminUser(user)) {
|
||||
return defaultRedirectPage(to, userPermissions);
|
||||
}
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
|
||||
|
||||
@@ -55,9 +55,11 @@ describe('#defaultRedirectPage', () => {
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
|
||||
});
|
||||
|
||||
it('should return dashboard route as default for users with custom roles', () => {
|
||||
it('should return the profile route for zero-permission custom roles', () => {
|
||||
const permissions = ['custom_role'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
expect(defaultRedirectPage(to, permissions)).toBe(
|
||||
'accounts/2/profile/settings'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with administrator role', () => {
|
||||
@@ -138,6 +140,28 @@ describe('#validateLoggedInRoutes', () => {
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
|
||||
it('redirects non-super-admin users from super-admin routes', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{
|
||||
name: 'super_admin_dashboard',
|
||||
params: { accountId: 1 },
|
||||
meta: { isSuperAdmin: true, permissions: ['agent'] },
|
||||
},
|
||||
{
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
role: 'agent',
|
||||
permissions: ['agent'],
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
});
|
||||
describe('when route is suspended route', () => {
|
||||
it('returns dashboard url', () => {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"ERROR": "Unable to load custom roles. Please try again.",
|
||||
"RETRY": "Retry",
|
||||
"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": {
|
||||
@@ -85,9 +87,9 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
"MESSAGE": "Deleting this role removes it from all assigned agents and restores standard Agent permissions; some permissions may increase. Continue?",
|
||||
"YES": "Yes, delete",
|
||||
"NO": "No, keep"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
},
|
||||
"LIST": {
|
||||
"404": "此账户中没有可用的自定义角色。",
|
||||
"ERROR": "自定义角色加载失败,请重试。",
|
||||
"RETRY": "重试",
|
||||
"TITLE": "管理自定义角色",
|
||||
"DESC": "自定义角色是由账户所有者或管理员创建的角色。这些角色可以分配给客服人员,以定义他们在账户中的访问权限和权限。自定义角色可以根据组织的需求创建特定的权限和访问级别。",
|
||||
"TABLE_HEADER": {
|
||||
@@ -85,7 +87,7 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "确认删除",
|
||||
"MESSAGE": "您确定要删除吗 ",
|
||||
"MESSAGE": "删除此角色后,所有使用该角色的客服将改用普通客服权限,部分权限可能增加。是否继续?",
|
||||
"YES": "是的,删除",
|
||||
"NO": "不,保留"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch, useTemplateRef } from 'vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { ROLES } from 'dashboard/constants/permissions';
|
||||
|
||||
import SearchInput from './SearchInput.vue';
|
||||
@@ -52,10 +51,6 @@ watch(
|
||||
>
|
||||
<Policy
|
||||
:permissions="ROLES"
|
||||
:installation-types="[
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
]"
|
||||
:feature-flag="FEATURE_FLAGS.ADVANCED_SEARCH"
|
||||
class="w-full"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
import CaptainPageRouteView from './pages/CaptainPageRouteView.vue';
|
||||
@@ -21,19 +20,16 @@ import CustomToolsIndex from './tools/Index.vue';
|
||||
const meta = {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
const metaCustomTools = {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS,
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
const metaV2 = {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
const assistantRoutes = [
|
||||
@@ -114,10 +110,6 @@ const assistantRoutes = [
|
||||
name: 'captain_assistants_create_index',
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,12 +2,10 @@ import { frontendURL } from '../../../helper/URLHelper';
|
||||
import CompaniesIndex from './pages/CompaniesIndex.vue';
|
||||
import CompanyDetailView from './pages/CompanyDetailView.vue';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
|
||||
const commonMeta = {
|
||||
featureFlag: FEATURE_FLAGS.COMPANIES,
|
||||
permissions: ['administrator', 'agent'],
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
export const routes = [
|
||||
|
||||
@@ -75,9 +75,11 @@ const addAgent = async () => {
|
||||
};
|
||||
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.role = 'agent';
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
payload.custom_role_id = null;
|
||||
}
|
||||
|
||||
const agent = await store.dispatch('agents/create', payload);
|
||||
|
||||
@@ -19,10 +19,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -126,7 +122,7 @@ const availabilityStatuses = computed(() =>
|
||||
|
||||
const editAgent = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
if (v$.value.$invalid || !selectedRole.value) return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
@@ -137,6 +133,7 @@ const editAgent = async () => {
|
||||
};
|
||||
|
||||
if (selectedRole.value.name.startsWith('custom_')) {
|
||||
payload.role = 'agent';
|
||||
payload.custom_role_id = selectedRole.value.id;
|
||||
} else {
|
||||
payload.role = selectedRole.value.name;
|
||||
@@ -248,7 +245,7 @@ const resetPassword = async () => {
|
||||
<Button
|
||||
type="submit"
|
||||
:label="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
|
||||
:disabled="v$.$invalid || uiFlags.isUpdating"
|
||||
:disabled="v$.$invalid || uiFlags.isUpdating || !selectedRole"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -49,10 +49,20 @@ const filteredAgentList = computed(() => {
|
||||
const uiFlags = computed(() => getters['agents/getUIFlags'].value);
|
||||
const currentUserId = computed(() => getters.getCurrentUserID.value);
|
||||
const customRoles = useMapGetter('customRole/getCustomRoles');
|
||||
const customRoleLoadError = ref(false);
|
||||
|
||||
const loadCustomRoles = async () => {
|
||||
customRoleLoadError.value = false;
|
||||
try {
|
||||
await store.dispatch('customRole/getCustomRole');
|
||||
} catch {
|
||||
customRoleLoadError.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('agents/get');
|
||||
store.dispatch('customRole/getCustomRole');
|
||||
loadCustomRoles();
|
||||
});
|
||||
|
||||
const findCustomRole = agent =>
|
||||
@@ -99,9 +109,11 @@ const showDeleteAction = agent => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const showAlertMessage = message => {
|
||||
loading.value[currentAgent.value.id] = false;
|
||||
currentAgent.value = {};
|
||||
const showAlertMessage = (message, id) => {
|
||||
loading.value[id] = false;
|
||||
if (currentAgent.value.id === id) {
|
||||
currentAgent.value = {};
|
||||
}
|
||||
agentAPI.value.message = message;
|
||||
useAlert(message);
|
||||
};
|
||||
@@ -132,15 +144,17 @@ const closeDeletePopup = () => {
|
||||
const deleteAgent = async id => {
|
||||
try {
|
||||
await store.dispatch('agents/delete', id);
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'), id);
|
||||
} catch (error) {
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'), id);
|
||||
}
|
||||
};
|
||||
const confirmDeletion = () => {
|
||||
loading.value[currentAgent.value.id] = true;
|
||||
const id = currentAgent.value.id;
|
||||
if (!id) return;
|
||||
loading.value[id] = true;
|
||||
closeDeletePopup();
|
||||
deleteAgent(currentAgent.value.id);
|
||||
deleteAgent(id);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -175,6 +189,19 @@ const confirmDeletion = () => {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<div
|
||||
v-if="customRoleLoadError"
|
||||
class="flex items-center justify-between gap-3 border-b border-n-weak px-4 py-3 text-sm text-n-slate-11"
|
||||
>
|
||||
<span>{{ $t('CUSTOM_ROLE.LIST.ERROR') }}</span>
|
||||
<Button
|
||||
slate
|
||||
faded
|
||||
size="sm"
|
||||
:label="$t('CUSTOM_ROLE.LIST.RETRY')"
|
||||
@click="loadCustomRoles"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-if="!filteredAgentList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
@@ -294,7 +321,6 @@ const confirmDeletion = () => {
|
||||
:name="currentAgent.name"
|
||||
:provider="currentAgent.provider"
|
||||
:type="currentAgent.role"
|
||||
:email="currentAgent.email"
|
||||
:availability="currentAgent.availability_status"
|
||||
:custom-role-id="currentAgent.custom_role_id"
|
||||
:active="currentAgent.active"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
@@ -22,10 +21,6 @@ export default {
|
||||
name: 'auditlogs_list',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: AuditLogsHome,
|
||||
|
||||
+8
-3
@@ -1,5 +1,5 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import store from 'dashboard/store';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
|
||||
@@ -9,7 +9,13 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/settings/billing'),
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD],
|
||||
},
|
||||
beforeEnter: to => {
|
||||
const isCloud = store.getters['globalConfig/isOnChatwootCloud'];
|
||||
const isCustomBranded =
|
||||
store.getters['globalConfig/isACustomBrandedInstance'];
|
||||
if (isCloud && !isCustomBranded) return true;
|
||||
return { name: 'home', params: { accountId: to.params.accountId } };
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
@@ -23,7 +29,6 @@ export default {
|
||||
name: 'billing_settings_index',
|
||||
component: Index,
|
||||
meta: {
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD],
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ const loading = ref({});
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const activeResponse = ref({});
|
||||
const searchQuery = ref('');
|
||||
const fetchError = ref(false);
|
||||
|
||||
const records = useMapGetter('customRole/getCustomRoles');
|
||||
|
||||
@@ -40,10 +41,6 @@ const deleteRejectText = computed(
|
||||
() => `${t('CUSTOM_ROLE.DELETE.CONFIRM.NO')} ${activeResponse.value.name}`
|
||||
);
|
||||
|
||||
const deleteMessage = computed(() => {
|
||||
return ` ${activeResponse.value.name} ? `;
|
||||
});
|
||||
|
||||
const isFeatureEnabledOnAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
@@ -58,10 +55,11 @@ const isBehindAPaywall = computed(() => {
|
||||
});
|
||||
|
||||
const fetchCustomRoles = async () => {
|
||||
fetchError.value = false;
|
||||
try {
|
||||
await store.dispatch('customRole/getCustomRole');
|
||||
} catch (error) {
|
||||
// Ignore Error
|
||||
} catch {
|
||||
fetchError.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,9 +76,11 @@ const tableHeaders = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const showAlertMessage = message => {
|
||||
loading.value[activeResponse.value.id] = false;
|
||||
activeResponse.value = {};
|
||||
const showAlertMessage = (message, id) => {
|
||||
loading.value[id] = false;
|
||||
if (activeResponse.value.id === id) {
|
||||
activeResponse.value = {};
|
||||
}
|
||||
useAlert(message);
|
||||
};
|
||||
|
||||
@@ -114,18 +114,20 @@ const closeDeletePopup = () => {
|
||||
const deleteCustomRole = async id => {
|
||||
try {
|
||||
await store.dispatch('customRole/deleteCustomRole', id);
|
||||
showAlertMessage(t('CUSTOM_ROLE.DELETE.API.SUCCESS_MESSAGE'));
|
||||
showAlertMessage(t('CUSTOM_ROLE.DELETE.API.SUCCESS_MESSAGE'), id);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('CUSTOM_ROLE.DELETE.API.ERROR_MESSAGE');
|
||||
showAlertMessage(errorMessage);
|
||||
showAlertMessage(errorMessage, id);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
loading[activeResponse.value.id] = true;
|
||||
const id = activeResponse.value.id;
|
||||
if (!id) return;
|
||||
loading.value[id] = true;
|
||||
closeDeletePopup();
|
||||
deleteCustomRole(activeResponse.value.id);
|
||||
deleteCustomRole(id);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -133,7 +135,7 @@ const confirmDeletion = () => {
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:loading-message="$t('CUSTOM_ROLE.LOADING')"
|
||||
:no-records-found="!records.length && !isBehindAPaywall"
|
||||
:no-records-found="!records.length && !isBehindAPaywall && !fetchError"
|
||||
:no-records-message="$t('CUSTOM_ROLE.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
@@ -141,13 +143,11 @@ const confirmDeletion = () => {
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('CUSTOM_ROLE.HEADER')"
|
||||
:description="$t('CUSTOM_ROLE.DESCRIPTION')"
|
||||
:link-text="$t('CUSTOM_ROLE.LEARN_MORE')"
|
||||
:search-placeholder="$t('CUSTOM_ROLE.SEARCH_PLACEHOLDER')"
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template v-if="records?.length" #count>
|
||||
<template v-if="filteredRecords?.length" #count>
|
||||
<span class="text-body-main text-n-slate-11">
|
||||
{{ $t('CUSTOM_ROLE.COUNT', { n: records.length }) }}
|
||||
{{ $t('CUSTOM_ROLE.COUNT', { n: filteredRecords.length }) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
@@ -162,7 +162,22 @@ const confirmDeletion = () => {
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<CustomRolePaywall v-if="isBehindAPaywall" />
|
||||
<div
|
||||
v-if="fetchError && !isBehindAPaywall"
|
||||
class="flex flex-col items-center justify-center gap-3 py-20"
|
||||
>
|
||||
<p class="text-base text-n-slate-12">
|
||||
{{ $t('CUSTOM_ROLE.LIST.ERROR') }}
|
||||
</p>
|
||||
<Button
|
||||
slate
|
||||
faded
|
||||
size="sm"
|
||||
:label="$t('CUSTOM_ROLE.LIST.RETRY')"
|
||||
@click="fetchCustomRoles"
|
||||
/>
|
||||
</div>
|
||||
<CustomRolePaywall v-else-if="isBehindAPaywall" />
|
||||
<BaseTable
|
||||
v-else
|
||||
:headers="tableHeaders"
|
||||
@@ -201,7 +216,6 @@ const confirmDeletion = () => {
|
||||
: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"
|
||||
/>
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import CustomRoleModal from './CustomRoleModal.vue';
|
||||
|
||||
vi.mock('dashboard/composables', () => ({ useAlert: vi.fn() }));
|
||||
const mockedStore = vi.hoisted(() => ({ dispatch: vi.fn() }));
|
||||
vi.mock('dashboard/composables/store', () => ({
|
||||
useStore: () => mockedStore,
|
||||
}));
|
||||
|
||||
const Button = {
|
||||
props: ['label', 'disabled', 'isLoading'],
|
||||
template: '<button :disabled="disabled">{{ label }}</button>',
|
||||
};
|
||||
|
||||
const mountModal = selectedRole =>
|
||||
shallowMount(CustomRoleModal, {
|
||||
props: { mode: 'edit', selectedRole },
|
||||
global: {
|
||||
stubs: {
|
||||
Button,
|
||||
'woot-modal-header': true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('CustomRoleModal', () => {
|
||||
it('preserves the existing conversation permissions while hydrating an edit form', async () => {
|
||||
const selectedRole = {
|
||||
name: 'Support',
|
||||
description: 'Support role',
|
||||
permissions: ['conversation_manage', 'contact_manage'],
|
||||
};
|
||||
const wrapper = mountModal(selectedRole);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('#conversation_manage').element.checked).toBe(true);
|
||||
expect(
|
||||
wrapper.find('#conversation_unassigned_manage').element.checked
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('#conversation_participating_manage').element.checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('does not mutate the role permissions when editing the form', async () => {
|
||||
const selectedRole = {
|
||||
name: 'Support',
|
||||
description: 'Support role',
|
||||
permissions: ['contact_manage'],
|
||||
};
|
||||
const wrapper = mountModal(selectedRole);
|
||||
|
||||
await wrapper.find('#report_manage').setValue(true);
|
||||
|
||||
expect(selectedRole.permissions).toEqual(['contact_manage']);
|
||||
});
|
||||
});
|
||||
+7
-2
@@ -34,6 +34,7 @@ const { t } = useI18n();
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
const selectedPermissions = ref([]);
|
||||
let isPopulatingEditForm = false;
|
||||
|
||||
const nameInput = ref(null);
|
||||
|
||||
@@ -58,14 +59,18 @@ const resetForm = () => {
|
||||
};
|
||||
|
||||
const populateEditForm = () => {
|
||||
isPopulatingEditForm = true;
|
||||
name.value = props.selectedRole.name || '';
|
||||
description.value = props.selectedRole.description || '';
|
||||
selectedPermissions.value = props.selectedRole.permissions || [];
|
||||
selectedPermissions.value = [...(props.selectedRole.permissions || [])];
|
||||
isPopulatingEditForm = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
selectedPermissions,
|
||||
(newValue, oldValue) => {
|
||||
if (isPopulatingEditForm) return;
|
||||
|
||||
// Check if manage all conversation permission is added or removed
|
||||
const hasAddedManageAllConversation =
|
||||
newValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS) &&
|
||||
@@ -92,7 +97,7 @@ watch(
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true, flush: 'sync' }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
-5
@@ -1,5 +1,4 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
@@ -20,10 +19,6 @@ export default {
|
||||
name: 'custom_roles_list',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.CUSTOM_ROLES,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: CustomRolesHome,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
@@ -8,7 +7,6 @@ import Index from './Index.vue';
|
||||
const meta = {
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
permissions: ['administrator'],
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -29,8 +29,10 @@ export const actions = {
|
||||
const response = await CustomRoleAPI.get();
|
||||
commit(types.default.SET_CUSTOM_ROLE, response.data);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,10 +70,10 @@ export const actions = {
|
||||
try {
|
||||
await CustomRoleAPI.delete(id);
|
||||
commit(types.default.DELETE_CUSTOM_ROLE, id);
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: false });
|
||||
return id;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
|
||||
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('#actions', () => {
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.getCustomRole({ commit });
|
||||
await expect(actions.getCustomRole({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false }],
|
||||
@@ -80,7 +80,7 @@ describe('#actions', () => {
|
||||
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 }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
@@ -90,7 +90,7 @@ describe('#actions', () => {
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
|
||||
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user