feat: complete current GoChat updates
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user