feat(ee): Add SLA management UI (#8777)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
9
app/javascript/dashboard/api/sla.js
Normal file
9
app/javascript/dashboard/api/sla.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class SlaAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('sla_policies', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SlaAPI();
|
||||||
@@ -39,6 +39,7 @@ const settings = accountId => ({
|
|||||||
'settings_teams_finish',
|
'settings_teams_finish',
|
||||||
'settings_teams_list',
|
'settings_teams_list',
|
||||||
'settings_teams_new',
|
'settings_teams_new',
|
||||||
|
'sla_list',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
@@ -158,6 +159,15 @@ const settings = accountId => ({
|
|||||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||||
beta: true,
|
beta: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'key',
|
||||||
|
label: 'SLA',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||||
|
toStateName: 'sla_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.SLA,
|
||||||
|
beta: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ export const FEATURE_FLAGS = {
|
|||||||
AUDIT_LOGS: 'audit_logs',
|
AUDIT_LOGS: 'audit_logs',
|
||||||
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
||||||
INBOX_VIEW: 'inbox_view',
|
INBOX_VIEW: 'inbox_view',
|
||||||
|
SLA: 'sla',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,3 +111,9 @@ export const INBOX_EVENTS = Object.freeze({
|
|||||||
DELETE_NOTIFICATION: 'Deleted notification',
|
DELETE_NOTIFICATION: 'Deleted notification',
|
||||||
DELETE_ALL_NOTIFICATIONS: 'Deleted all notifications',
|
DELETE_ALL_NOTIFICATIONS: 'Deleted all notifications',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SLA_EVENTS = Object.freeze({
|
||||||
|
CREATE: 'Created an SLA',
|
||||||
|
UPDATE: 'Updated an SLA',
|
||||||
|
DELETED: 'Deleted an SLA',
|
||||||
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import settings from './settings.json';
|
|||||||
import signup from './signup.json';
|
import signup from './signup.json';
|
||||||
import teamsSettings from './teamsSettings.json';
|
import teamsSettings from './teamsSettings.json';
|
||||||
import whatsappTemplates from './whatsappTemplates.json';
|
import whatsappTemplates from './whatsappTemplates.json';
|
||||||
|
import sla from './sla.json';
|
||||||
import inbox from './inbox.json';
|
import inbox from './inbox.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -61,6 +62,7 @@ export default {
|
|||||||
...setNewPassword,
|
...setNewPassword,
|
||||||
...settings,
|
...settings,
|
||||||
...signup,
|
...signup,
|
||||||
|
...sla,
|
||||||
...teamsSettings,
|
...teamsSettings,
|
||||||
...whatsappTemplates,
|
...whatsappTemplates,
|
||||||
...inbox,
|
...inbox,
|
||||||
|
|||||||
@@ -238,6 +238,7 @@
|
|||||||
"REPORTS_INBOX": "Inbox",
|
"REPORTS_INBOX": "Inbox",
|
||||||
"REPORTS_TEAM": "Team",
|
"REPORTS_TEAM": "Team",
|
||||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||||
|
"SLA": "SLA",
|
||||||
"BETA": "Beta",
|
"BETA": "Beta",
|
||||||
"REPORTS_OVERVIEW": "Overview",
|
"REPORTS_OVERVIEW": "Overview",
|
||||||
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||||
|
|||||||
63
app/javascript/dashboard/i18n/locale/en/sla.json
Normal file
63
app/javascript/dashboard/i18n/locale/en/sla.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"SLA": {
|
||||||
|
"HEADER": "SLA",
|
||||||
|
"HEADER_BTN_TXT": "Add SLA",
|
||||||
|
"LOADING": "Fetching SLAs",
|
||||||
|
"SEARCH_404": "There are no items matching this query",
|
||||||
|
"SIDEBAR_TXT": "<p><b>SLA</b> <p>Think of Service Level Agreements (SLAs) like friendly promises between a service provider and a customer.</p> <p> These promises set clear expectations for things like how quickly the team will respond to issues, making sure you always get a reliable and top-notch experience!</p>",
|
||||||
|
"LIST": {
|
||||||
|
"404": "There are no SLAs available in this account.",
|
||||||
|
"TITLE": "Manage SLA",
|
||||||
|
"DESC": "SLAs: Friendly promises for great service!",
|
||||||
|
"TABLE_HEADER": ["Name", "Description", "FRT", "NRT", "RT", "Business Hours"]
|
||||||
|
},
|
||||||
|
"FORM": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "SLA Name",
|
||||||
|
"PLACEHOLDER": "SLA Name",
|
||||||
|
"REQUIRED_ERROR": "SLA name is required",
|
||||||
|
"MINIMUM_LENGTH_ERROR": "Minimum length 2 is required",
|
||||||
|
"VALID_ERROR": "Only Alphabets, Numbers, Hyphen and Underscore are allowed"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description",
|
||||||
|
"PLACEHOLDER": "SLA for premium customers"
|
||||||
|
},
|
||||||
|
"FIRST_RESPONSE_TIME": {
|
||||||
|
"LABEL": "First Response Time(Seconds)",
|
||||||
|
"PLACEHOLDER": "300 for 5 minutes"
|
||||||
|
},
|
||||||
|
"NEXT_RESPONSE_TIME": {
|
||||||
|
"LABEL": "Next Response Time(Seconds)",
|
||||||
|
"PLACEHOLDER": "600 for 10 minutes"
|
||||||
|
},
|
||||||
|
"RESOLUTION_TIME": {
|
||||||
|
"LABEL": "Resolution Time(Seconds)",
|
||||||
|
"PLACEHOLDER": "86400 for 1 day"
|
||||||
|
},
|
||||||
|
"BUSINESS_HOURS": {
|
||||||
|
"LABEL": "Business Hours",
|
||||||
|
"PLACEHOLDER": "Only during business hours"
|
||||||
|
},
|
||||||
|
"EDIT": "Edit",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"DELETE": "Delete",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"TITLE": "Add SLA",
|
||||||
|
"DESC": "SLAs: Friendly promises for great service!",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "SLA added successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"TITLE": "Edit SLA",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "SLA updated successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import macros from './macros/macros.routes';
|
|||||||
import profile from './profile/profile.routes';
|
import profile from './profile/profile.routes';
|
||||||
import reports from './reports/reports.routes';
|
import reports from './reports/reports.routes';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import sla from './sla/sla.routes';
|
||||||
import teams from './teams/teams.routes';
|
import teams from './teams/teams.routes';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -47,6 +48,7 @@ export default {
|
|||||||
...macros.routes,
|
...macros.routes,
|
||||||
...profile.routes,
|
...profile.routes,
|
||||||
...reports.routes,
|
...reports.routes,
|
||||||
|
...sla.routes,
|
||||||
...teams.routes,
|
...teams.routes,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-auto overflow-auto flex flex-col">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('SLA.ADD.TITLE')"
|
||||||
|
:header-content="$t('SLA.ADD.DESC')"
|
||||||
|
/>
|
||||||
|
<sla-form
|
||||||
|
:submit-label="$t('SLA.FORM.CREATE')"
|
||||||
|
@submit="addSLA"
|
||||||
|
@close="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import validationMixin from './validationMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import validations from './validations';
|
||||||
|
import SlaForm from './SlaForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SlaForm,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, validationMixin],
|
||||||
|
validations,
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'sla/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async addSLA(payload) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('sla/create', payload);
|
||||||
|
this.showAlert(this.$t('SLA.ADD.API.SUCCESS_MESSAGE'));
|
||||||
|
this.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error.message || this.$t('SLA.ADD.API.ERROR_MESSAGE');
|
||||||
|
this.showAlert(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-auto overflow-auto flex flex-col">
|
||||||
|
<woot-modal-header :header-title="pageTitle" />
|
||||||
|
<sla-form
|
||||||
|
:submit-label="$t('SLA.FORM.EDIT')"
|
||||||
|
:selected-response="selectedResponse"
|
||||||
|
@submit="editSLA"
|
||||||
|
@close="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import validationMixin from './validationMixin';
|
||||||
|
import validations from './validations';
|
||||||
|
import SlaForm from './SlaForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SlaForm,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, validationMixin],
|
||||||
|
props: {
|
||||||
|
selectedResponse: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations,
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'sla/getUIFlags',
|
||||||
|
}),
|
||||||
|
pageTitle() {
|
||||||
|
return `${this.$t('SLA.EDIT.TITLE')} - ${this.selectedResponse.name}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async editSLA(payload) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('sla/update', {
|
||||||
|
id: this.selectedResponse.id,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
this.showAlert(this.$t('SLA.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
this.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error.message || this.$t('SLA.EDIT.API.ERROR_MESSAGE');
|
||||||
|
this.showAlert(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
135
app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue
Normal file
135
app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1 overflow-auto p-4">
|
||||||
|
<woot-button
|
||||||
|
color-scheme="success"
|
||||||
|
class-names="button--fixed-top"
|
||||||
|
icon="add-circle"
|
||||||
|
@click="openAddPopup"
|
||||||
|
>
|
||||||
|
{{ $t('SLA.HEADER_BTN_TXT') }}
|
||||||
|
</woot-button>
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<div class="w-[60%]">
|
||||||
|
<p
|
||||||
|
v-if="!uiFlags.isFetching && !records.length"
|
||||||
|
class="flex h-full items-center flex-col justify-center"
|
||||||
|
>
|
||||||
|
{{ $t('SLA.LIST.404') }}
|
||||||
|
</p>
|
||||||
|
<woot-loading-state
|
||||||
|
v-if="uiFlags.isFetching"
|
||||||
|
:message="$t('SLA.LOADING')"
|
||||||
|
/>
|
||||||
|
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
||||||
|
<thead>
|
||||||
|
<th v-for="thHeader in $t('SLA.LIST.TABLE_HEADER')" :key="thHeader">
|
||||||
|
{{ thHeader }}
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="sla in records" :key="sla.title">
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="inline-block overflow-hidden whitespace-nowrap text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ sla.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ sla.description }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="flex items-center">
|
||||||
|
{{ sla.first_response_time_threshold }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="flex items-center">
|
||||||
|
{{ sla.next_response_time_threshold }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="flex items-center">
|
||||||
|
{{ sla.resolution_time_threshold }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="flex items-center">
|
||||||
|
{{ sla.only_during_business_hours }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="button-wrapper">
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top="$t('SLA.FORM.EDIT')"
|
||||||
|
variant="smooth"
|
||||||
|
size="tiny"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class-names="grey-btn"
|
||||||
|
:is-loading="loading[sla.id]"
|
||||||
|
icon="edit"
|
||||||
|
@click="openEditPopup(sla)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-[34%]">
|
||||||
|
<span v-dompurify-html="$t('SLA.SIDEBAR_TXT')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||||
|
<add-SLA @close="hideAddPopup" />
|
||||||
|
</woot-modal>
|
||||||
|
|
||||||
|
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||||
|
<edit-SLA :selected-response="selectedResponse" @close="hideEditPopup" />
|
||||||
|
</woot-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import AddSLA from './AddSLA.vue';
|
||||||
|
import EditSLA from './EditSLA.vue';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AddSLA,
|
||||||
|
EditSLA,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: {},
|
||||||
|
showAddPopup: false,
|
||||||
|
showEditPopup: false,
|
||||||
|
selectedResponse: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
records: 'sla/getSLA',
|
||||||
|
uiFlags: 'sla/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('sla/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openAddPopup() {
|
||||||
|
this.showAddPopup = true;
|
||||||
|
},
|
||||||
|
hideAddPopup() {
|
||||||
|
this.showAddPopup = false;
|
||||||
|
},
|
||||||
|
openEditPopup(response) {
|
||||||
|
this.showEditPopup = true;
|
||||||
|
this.selectedResponse = response;
|
||||||
|
},
|
||||||
|
hideEditPopup() {
|
||||||
|
this.showEditPopup = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-auto overflow-auto flex flex-col">
|
||||||
|
<form class="mx-0 flex flex-wrap" @submit.prevent="onSubmit">
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="name"
|
||||||
|
:class="{ error: $v.name.$error }"
|
||||||
|
class="w-full"
|
||||||
|
:sla="$t('SLA.FORM.NAME.LABEL')"
|
||||||
|
:placeholder="$t('SLA.FORM.NAME.PLACEHOLDER')"
|
||||||
|
:error="getSlaNameErrorMessage"
|
||||||
|
@input="$v.name.$touch"
|
||||||
|
/>
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="description"
|
||||||
|
class="w-full"
|
||||||
|
:label="$t('SLA.FORM.DESCRIPTION.LABEL')"
|
||||||
|
:placeholder="$t('SLA.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||||
|
data-testid="sla-description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="firstResponseTimeThreshold"
|
||||||
|
class="w-full"
|
||||||
|
:label="$t('SLA.FORM.FIRST_RESPONSE_TIME.LABEL')"
|
||||||
|
:placeholder="$t('SLA.FORM.FIRST_RESPONSE_TIME.PLACEHOLDER')"
|
||||||
|
data-testid="sla-firstResponseTimeThreshold"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="nextResponseTimeThreshold"
|
||||||
|
class="w-full"
|
||||||
|
:label="$t('SLA.FORM.NEXT_RESPONSE_TIME.LABEL')"
|
||||||
|
:placeholder="$t('SLA.FORM.NEXT_RESPONSE_TIME.PLACEHOLDER')"
|
||||||
|
data-testid="sla-nextResponseTimeThreshold"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="resolutionTimeThreshold"
|
||||||
|
class="w-full"
|
||||||
|
:label="$t('SLA.FORM.RESOLUTION_TIME.LABEL')"
|
||||||
|
:placeholder="$t('SLA.FORM.RESOLUTION_TIME.PLACEHOLDER')"
|
||||||
|
data-testid="sla-resolutionTimeThreshold"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<input id="sla_bh" v-model="onlyDuringBusinessHours" type="checkbox" />
|
||||||
|
<label for="sla_bh">
|
||||||
|
{{ $t('SLA.FORM.BUSINESS_HOURS.PLACEHOLDER') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end items-center py-2 px-0 gap-2 w-full">
|
||||||
|
<woot-button
|
||||||
|
:is-disabled="$v.name.$invalid || uiFlags.isUpdating"
|
||||||
|
:is-loading="uiFlags.isUpdating"
|
||||||
|
>
|
||||||
|
{{ submitLabel }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button class="button clear" @click.prevent="onClose">
|
||||||
|
{{ $t('SLA.FORM.CANCEL') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import validationMixin from './validationMixin';
|
||||||
|
import validations from './validations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [validationMixin],
|
||||||
|
props: {
|
||||||
|
selectedResponse: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
submitLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
firstResponseTimeThreshold: '',
|
||||||
|
nextResponseTimeThreshold: '',
|
||||||
|
resolutionTimeThreshold: '',
|
||||||
|
onlyDuringBusinessHours: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations,
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'sla/getUIFlags',
|
||||||
|
}),
|
||||||
|
pageTitle() {
|
||||||
|
return `${this.$t('SLA.EDIT.TITLE')} - ${
|
||||||
|
this.selectedResponse?.name || ''
|
||||||
|
}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.selectedResponse) this.setFormValues();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
setFormValues() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
first_response_time_threshold: firstResponseTimeThreshold,
|
||||||
|
next_response_time_threshold: nextResponseTimeThreshold,
|
||||||
|
resolution_time_threshold: resolutionTimeThreshold,
|
||||||
|
only_during_business_hours: onlyDuringBusinessHours,
|
||||||
|
} = this.selectedResponse;
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.firstResponseTimeThreshold = firstResponseTimeThreshold;
|
||||||
|
this.nextResponseTimeThreshold = nextResponseTimeThreshold;
|
||||||
|
this.resolutionTimeThreshold = resolutionTimeThreshold;
|
||||||
|
this.onlyDuringBusinessHours = onlyDuringBusinessHours;
|
||||||
|
},
|
||||||
|
onSubmit() {
|
||||||
|
this.$emit('submit', {
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
first_response_time_threshold: this.firstResponseTimeThreshold,
|
||||||
|
next_response_time_threshold: this.nextResponseTimeThreshold,
|
||||||
|
resolution_time_threshold: this.resolutionTimeThreshold,
|
||||||
|
only_during_business_hours: this.onlyDuringBusinessHours,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
|
const SettingsContent = () => import('../Wrapper.vue');
|
||||||
|
const Index = () => import('./Index.vue');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/settings/sla'),
|
||||||
|
component: SettingsContent,
|
||||||
|
props: {
|
||||||
|
headerTitle: 'SLA.HEADER',
|
||||||
|
icon: 'tag',
|
||||||
|
showNewButton: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'sla_wrapper',
|
||||||
|
roles: ['administrator'],
|
||||||
|
redirect: 'list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'sla_list',
|
||||||
|
roles: ['administrator'],
|
||||||
|
component: Index,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import VueI18n from 'vue-i18n';
|
||||||
|
import Vuelidate from 'vuelidate';
|
||||||
|
|
||||||
|
import validationMixin from '../validationMixin';
|
||||||
|
import validations from '../validations';
|
||||||
|
import i18n from 'dashboard/i18n';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(VueI18n);
|
||||||
|
localVue.use(Vuelidate);
|
||||||
|
|
||||||
|
const i18nConfig = new VueI18n({
|
||||||
|
locale: 'en',
|
||||||
|
messages: i18n,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TestComponent = {
|
||||||
|
render() {},
|
||||||
|
mixins: [validationMixin],
|
||||||
|
validations,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('validationMixin', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallowMount(TestComponent, {
|
||||||
|
localVue,
|
||||||
|
i18n: i18nConfig,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return required error message if name is empty but not touched', () => {
|
||||||
|
wrapper.setData({ name: '' });
|
||||||
|
expect(wrapper.vm.getSlaNameErrorMessage).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty error message if name is valid', () => {
|
||||||
|
wrapper.setData({ name: 'ValidName' });
|
||||||
|
wrapper.vm.$v.name.$touch();
|
||||||
|
expect(wrapper.vm.getSlaNameErrorMessage).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return required error message if name is empty', () => {
|
||||||
|
wrapper.setData({ name: '' });
|
||||||
|
wrapper.vm.$v.name.$touch();
|
||||||
|
expect(wrapper.vm.getSlaNameErrorMessage).toBe(
|
||||||
|
wrapper.vm.$t('SLA.FORM.NAME.REQUIRED_ERROR')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return minimum length error message if name is too short', () => {
|
||||||
|
wrapper.setData({ name: 'a' });
|
||||||
|
wrapper.vm.$v.name.$touch();
|
||||||
|
expect(wrapper.vm.getSlaNameErrorMessage).toBe(
|
||||||
|
wrapper.vm.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
getSlaNameErrorMessage() {
|
||||||
|
let errorMessage = '';
|
||||||
|
if (this.$v.name.$error) {
|
||||||
|
if (!this.$v.name.required) {
|
||||||
|
errorMessage = this.$t('SLA.FORM.NAME.REQUIRED_ERROR');
|
||||||
|
} else if (!this.$v.name.minLength) {
|
||||||
|
errorMessage = this.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(2),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@ import macros from './modules/macros';
|
|||||||
import notifications from './modules/notifications';
|
import notifications from './modules/notifications';
|
||||||
import portals from './modules/helpCenterPortals';
|
import portals from './modules/helpCenterPortals';
|
||||||
import reports from './modules/reports';
|
import reports from './modules/reports';
|
||||||
|
import sla from './modules/sla';
|
||||||
import teamMembers from './modules/teamMembers';
|
import teamMembers from './modules/teamMembers';
|
||||||
import teams from './modules/teams';
|
import teams from './modules/teams';
|
||||||
import userNotificationSettings from './modules/userNotificationSettings';
|
import userNotificationSettings from './modules/userNotificationSettings';
|
||||||
@@ -109,6 +110,7 @@ export default new Vuex.Store({
|
|||||||
userNotificationSettings,
|
userNotificationSettings,
|
||||||
webhooks,
|
webhooks,
|
||||||
draftMessages,
|
draftMessages,
|
||||||
|
sla,
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|||||||
86
app/javascript/dashboard/store/modules/sla.js
Normal file
86
app/javascript/dashboard/store/modules/sla.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
|
import types from '../mutation-types';
|
||||||
|
import SlaAPI from '../../api/sla';
|
||||||
|
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||||
|
import { SLA_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||||
|
import { throwErrorMessage } from '../utils/api';
|
||||||
|
|
||||||
|
export const state = {
|
||||||
|
records: [],
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
isFetchingItem: false,
|
||||||
|
isCreating: false,
|
||||||
|
isDeleting: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getSLA(_state) {
|
||||||
|
return _state.records;
|
||||||
|
},
|
||||||
|
getUIFlags(_state) {
|
||||||
|
return _state.uiFlags;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async function get({ commit }) {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const response = await SlaAPI.get();
|
||||||
|
commit(types.SET_SLA, response.data.payload);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async function create({ commit }, slaObj) {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isCreating: true });
|
||||||
|
try {
|
||||||
|
const response = await SlaAPI.create(slaObj);
|
||||||
|
AnalyticsHelper.track(SLA_EVENTS.CREATE);
|
||||||
|
commit(types.ADD_SLA, response.data.payload);
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isCreating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async function update({ commit }, { id, ...updateObj }) {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isUpdating: true });
|
||||||
|
try {
|
||||||
|
const response = await SlaAPI.update(id, updateObj);
|
||||||
|
AnalyticsHelper.track(SLA_EVENTS.UPDATE);
|
||||||
|
commit(types.EDIT_SLA, response.data.payload);
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_SLA_UI_FLAG, { isUpdating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_SLA_UI_FLAG](_state, data) {
|
||||||
|
_state.uiFlags = {
|
||||||
|
..._state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_SLA]: MutationHelpers.set,
|
||||||
|
[types.ADD_SLA]: MutationHelpers.create,
|
||||||
|
[types.EDIT_SLA]: MutationHelpers.update,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
||||||
@@ -301,4 +301,11 @@ export default {
|
|||||||
SET_AUDIT_LOGS_UI_FLAG: 'SET_AUDIT_LOGS_UI_FLAG',
|
SET_AUDIT_LOGS_UI_FLAG: 'SET_AUDIT_LOGS_UI_FLAG',
|
||||||
SET_AUDIT_LOGS: 'SET_AUDIT_LOGS',
|
SET_AUDIT_LOGS: 'SET_AUDIT_LOGS',
|
||||||
SET_AUDIT_LOGS_META: 'SET_AUDIT_LOGS_META',
|
SET_AUDIT_LOGS_META: 'SET_AUDIT_LOGS_META',
|
||||||
|
|
||||||
|
// SLA
|
||||||
|
SET_SLA_UI_FLAG: 'SET_SLA_UI_FLAG',
|
||||||
|
SET_SLA: 'SET_SLA',
|
||||||
|
ADD_SLA: 'ADD_SLA',
|
||||||
|
EDIT_SLA: 'EDIT_SLA',
|
||||||
|
DELETE_SLA: 'DELETE_SLA',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,3 +66,6 @@
|
|||||||
enabled: false
|
enabled: false
|
||||||
- name: inbox_view
|
- name: inbox_view
|
||||||
enabled: false
|
enabled: false
|
||||||
|
- name: sla
|
||||||
|
enabled: false
|
||||||
|
premium: true
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
- disable_branding
|
- disable_branding
|
||||||
- audit_logs
|
- audit_logs
|
||||||
- response_bot
|
- response_bot
|
||||||
|
- sla
|
||||||
|
|||||||
Reference in New Issue
Block a user