feat: Update the input for the SLA threshold selection (#8974)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -24,21 +24,24 @@
|
||||
"PLACEHOLDER": "SLA for premium customers"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"LABEL": "First Response Time(Seconds)",
|
||||
"PLACEHOLDER": "300 for 5 minutes"
|
||||
"LABEL": "First Response Time",
|
||||
"PLACEHOLDER": "5"
|
||||
},
|
||||
"NEXT_RESPONSE_TIME": {
|
||||
"LABEL": "Next Response Time(Seconds)",
|
||||
"PLACEHOLDER": "600 for 10 minutes"
|
||||
"LABEL": "Next Response Time",
|
||||
"PLACEHOLDER": "5"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"LABEL": "Resolution Time(Seconds)",
|
||||
"PLACEHOLDER": "86400 for 1 day"
|
||||
"LABEL": "Resolution Time",
|
||||
"PLACEHOLDER": "60"
|
||||
},
|
||||
"BUSINESS_HOURS": {
|
||||
"LABEL": "Business Hours",
|
||||
"PLACEHOLDER": "Only during business hours"
|
||||
},
|
||||
"THRESHOLD_TIME": {
|
||||
"INVALID_FORMAT_ERROR": "Threshold should be a number and greater than zero"
|
||||
},
|
||||
"EDIT": "Edit",
|
||||
"CREATE": "Create",
|
||||
"DELETE": "Delete",
|
||||
|
||||
@@ -38,17 +38,17 @@
|
||||
<td>{{ sla.description }}</td>
|
||||
<td>
|
||||
<span class="flex items-center">
|
||||
{{ sla.first_response_time_threshold }}
|
||||
{{ displayTime(sla.first_response_time_threshold) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="flex items-center">
|
||||
{{ sla.next_response_time_threshold }}
|
||||
{{ displayTime(sla.next_response_time_threshold) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="flex items-center">
|
||||
{{ sla.resolution_time_threshold }}
|
||||
{{ displayTime(sla.resolution_time_threshold) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -88,6 +88,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
|
||||
import AddSLA from './AddSLA.vue';
|
||||
import EditSLA from './EditSLA.vue';
|
||||
@@ -111,26 +112,12 @@ export default {
|
||||
...mapGetters({
|
||||
records: 'sla/getSLA',
|
||||
uiFlags: 'sla/getUIFlags',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
isSLAEnabled() {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, 'sla');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isSLAfeatureEnabled();
|
||||
this.$store.dispatch('sla/get');
|
||||
},
|
||||
methods: {
|
||||
isSLAfeatureEnabled() {
|
||||
if (!this.isSLAEnabled) {
|
||||
this.$router.push({
|
||||
name: 'general_settings_index',
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('sla/get');
|
||||
}
|
||||
},
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
@@ -144,6 +131,15 @@ export default {
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
displayTime(threshold) {
|
||||
const { time, unit } = convertSecondsToTimeUnit(threshold, {
|
||||
minute: 'm',
|
||||
hour: 'h',
|
||||
day: 'd',
|
||||
});
|
||||
if (!time) return '-';
|
||||
return `${time}${unit}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,44 +2,31 @@
|
||||
<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"
|
||||
v-model="name"
|
||||
:class="{ error: $v.name.$error }"
|
||||
class="w-full"
|
||||
:sla="$t('SLA.FORM.NAME.LABEL')"
|
||||
:label="$t('SLA.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('SLA.FORM.NAME.PLACEHOLDER')"
|
||||
:error="getSlaNameErrorMessage"
|
||||
@input="$v.name.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="description"
|
||||
v-model="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"
|
||||
<sla-time-input
|
||||
v-for="(input, index) in slaTimeInputs"
|
||||
:key="index"
|
||||
:threshold="input.threshold"
|
||||
:threshold-unit="input.unit"
|
||||
:label="$t(input.label)"
|
||||
:placeholder="$t(input.placeholder)"
|
||||
@input="updateThreshold(index, $event)"
|
||||
@unit="updateUnit(index, $event)"
|
||||
@isInValid="handleIsInvalid(index, $event)"
|
||||
/>
|
||||
|
||||
<div class="w-full">
|
||||
@@ -51,7 +38,7 @@
|
||||
|
||||
<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-disabled="isSubmitDisabled"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
@@ -66,10 +53,15 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
import validationMixin from './validationMixin';
|
||||
import validations from './validations';
|
||||
import SlaTimeInput from './SlaTimeInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SlaTimeInput,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
selectedResponse: {
|
||||
@@ -85,9 +77,28 @@ export default {
|
||||
return {
|
||||
name: '',
|
||||
description: '',
|
||||
firstResponseTimeThreshold: '',
|
||||
nextResponseTimeThreshold: '',
|
||||
resolutionTimeThreshold: '',
|
||||
isSlaTimeInputsInvalid: false,
|
||||
slaTimeInputsValidation: {},
|
||||
slaTimeInputs: [
|
||||
{
|
||||
threshold: null,
|
||||
unit: 'Minutes',
|
||||
label: 'SLA.FORM.FIRST_RESPONSE_TIME.LABEL',
|
||||
placeholder: 'SLA.FORM.FIRST_RESPONSE_TIME.PLACEHOLDER',
|
||||
},
|
||||
{
|
||||
threshold: null,
|
||||
unit: 'Minutes',
|
||||
label: 'SLA.FORM.NEXT_RESPONSE_TIME.LABEL',
|
||||
placeholder: 'SLA.FORM.NEXT_RESPONSE_TIME.PLACEHOLDER',
|
||||
},
|
||||
{
|
||||
threshold: null,
|
||||
unit: 'Minutes',
|
||||
label: 'SLA.FORM.RESOLUTION_TIME.LABEL',
|
||||
placeholder: 'SLA.FORM.RESOLUTION_TIME.PLACEHOLDER',
|
||||
},
|
||||
],
|
||||
onlyDuringBusinessHours: false,
|
||||
};
|
||||
},
|
||||
@@ -96,10 +107,12 @@ export default {
|
||||
...mapGetters({
|
||||
uiFlags: 'sla/getUIFlags',
|
||||
}),
|
||||
pageTitle() {
|
||||
return `${this.$t('SLA.EDIT.TITLE')} - ${
|
||||
this.selectedResponse?.name || ''
|
||||
}`;
|
||||
isSubmitDisabled() {
|
||||
return (
|
||||
this.$v.name.$invalid ||
|
||||
this.isSlaTimeInputsInvalid ||
|
||||
this.uiFlags.isUpdating
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -121,20 +134,59 @@ export default {
|
||||
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.firstResponseTimeThreshold = firstResponseTimeThreshold;
|
||||
this.nextResponseTimeThreshold = nextResponseTimeThreshold;
|
||||
this.resolutionTimeThreshold = resolutionTimeThreshold;
|
||||
this.onlyDuringBusinessHours = onlyDuringBusinessHours;
|
||||
|
||||
const thresholds = [
|
||||
firstResponseTimeThreshold,
|
||||
nextResponseTimeThreshold,
|
||||
resolutionTimeThreshold,
|
||||
];
|
||||
this.slaTimeInputs.forEach((input, index) => {
|
||||
const converted = convertSecondsToTimeUnit(thresholds[index], {
|
||||
minute: 'Minutes',
|
||||
hour: 'Hours',
|
||||
day: 'Days',
|
||||
});
|
||||
input.threshold = converted.time;
|
||||
input.unit = converted.unit;
|
||||
});
|
||||
},
|
||||
updateThreshold(index, value) {
|
||||
this.slaTimeInputs[index].threshold = value;
|
||||
},
|
||||
updateUnit(index, unit) {
|
||||
this.slaTimeInputs[index].unit = unit;
|
||||
},
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
const payload = {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
first_response_time_threshold: this.firstResponseTimeThreshold,
|
||||
next_response_time_threshold: this.nextResponseTimeThreshold,
|
||||
resolution_time_threshold: this.resolutionTimeThreshold,
|
||||
first_response_time_threshold: this.convertToSeconds(0),
|
||||
next_response_time_threshold: this.convertToSeconds(1),
|
||||
resolution_time_threshold: this.convertToSeconds(2),
|
||||
only_during_business_hours: this.onlyDuringBusinessHours,
|
||||
});
|
||||
};
|
||||
this.$emit('submit', payload);
|
||||
},
|
||||
convertToSeconds(index) {
|
||||
const { threshold, unit } = this.slaTimeInputs[index];
|
||||
if (threshold === null || threshold === 0) return null;
|
||||
const unitsToSeconds = { Minutes: 60, Hours: 3600, Days: 86400 };
|
||||
return Number(threshold * (unitsToSeconds[unit] || 1));
|
||||
},
|
||||
handleIsInvalid(index, isInvalid) {
|
||||
this.slaTimeInputsValidation = {
|
||||
...this.slaTimeInputsValidation,
|
||||
[index]: isInvalid,
|
||||
};
|
||||
|
||||
this.checkValidationState();
|
||||
},
|
||||
checkValidationState() {
|
||||
const isAnyInvalid = Object.values(this.slaTimeInputsValidation).some(
|
||||
isInvalid => isInvalid
|
||||
);
|
||||
this.isSlaTimeInputsInvalid = isAnyInvalid;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="relative mt-2 w-full">
|
||||
<woot-input
|
||||
v-model="thresholdTime"
|
||||
:class="{ error: $v.thresholdTime.$error }"
|
||||
class="w-full [&>input]:pr-24"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:error="getThresholdTimeErrorMessage"
|
||||
@input="onThresholdTimeChange"
|
||||
/>
|
||||
<div class="absolute right-px h-9 top-[27px] flex items-center">
|
||||
<select
|
||||
v-model="thresholdUnitValue"
|
||||
class="h-full rounded-[4px] hover:cursor-pointer font-medium border-1 border-solid bg-transparent border-transparent dark:border-transparent mb-0 py-0 pl-2 pr-7 text-slate-600 dark:text-slate-300 dark:focus:border-woot-500 focus:border-woot-500 text-sm"
|
||||
@change="onThresholdUnitChange"
|
||||
>
|
||||
<option
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import validationMixin from './validationMixin';
|
||||
import validations from './validations';
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
threshold: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
thresholdUnit: {
|
||||
type: String,
|
||||
default: 'Minutes',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
thresholdTime: this.threshold || '',
|
||||
thresholdUnitValue: this.thresholdUnit,
|
||||
options: [
|
||||
{ value: 'Minutes', label: 'Minutes' },
|
||||
{ value: 'Hours', label: 'Hours' },
|
||||
{ value: 'Days', label: 'Days' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations,
|
||||
watch: {
|
||||
threshold: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (!Number.isNaN(value)) {
|
||||
this.thresholdTime = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
thresholdUnit: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
this.thresholdUnitValue = value;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onThresholdUnitChange() {
|
||||
this.$emit('unit', this.thresholdUnitValue);
|
||||
},
|
||||
onThresholdTimeChange() {
|
||||
this.$v.thresholdTime.$touch();
|
||||
const isInvalid = this.$v.thresholdTime.$invalid;
|
||||
this.$emit('isInValid', isInvalid);
|
||||
this.$emit('input', Number(this.thresholdTime));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -31,6 +31,7 @@ describe('validationMixin', () => {
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
thresholdTime: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -62,4 +63,44 @@ describe('validationMixin', () => {
|
||||
wrapper.vm.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: 10 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t(''));
|
||||
|
||||
wrapper.setData({ thresholdTime: 10.5 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t(''));
|
||||
});
|
||||
|
||||
it('should not return invalid format error message if thresholdTime is empty but not touched', () => {
|
||||
wrapper.setData({ thresholdTime: '' });
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should return invalid format error message if thresholdTime has an invalid format', () => {
|
||||
wrapper.setData({ thresholdTime: 'fsdfsdfsdfsd' });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: 0 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: -1 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,5 +11,16 @@ export default {
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
getThresholdTimeErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (this.$v.thresholdTime.$error) {
|
||||
if (!this.$v.thresholdTime.numeric || !this.$v.thresholdTime.minValue) {
|
||||
errorMessage = this.$t(
|
||||
'SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
minValue,
|
||||
decimal,
|
||||
} from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
thresholdTime: {
|
||||
decimal,
|
||||
minValue: minValue(0.001),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "1.0.5",
|
||||
"@chatwoot/utils": "^0.0.21",
|
||||
"@chatwoot/utils": "^0.0.23",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
"@radix-ui/colors": "^1.0.1",
|
||||
|
||||
@@ -3177,10 +3177,10 @@
|
||||
prosemirror-utils "^0.9.6"
|
||||
prosemirror-view "^1.17.2"
|
||||
|
||||
"@chatwoot/utils@^0.0.21":
|
||||
version "0.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.21.tgz#f9116daac0514a8a8fa6ce594efff10062222be0"
|
||||
integrity sha512-eUDJ1K5x1rFlBywRctU3hXXiJ1U0EZiklowNl/YJOh1/BWDns4It3DWrQmAcjvsNbEUNWMfY+ShJmjdeei71Cw==
|
||||
"@chatwoot/utils@^0.0.23":
|
||||
version "0.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.23.tgz#e961fd87ef9ee19c442bfcedac5fe0be2ef37726"
|
||||
integrity sha512-BQ7DprXr7FIkSbHdDc1WonwH0rt/+B+WaaLaXNMjCcxJgkX/gZ8QMltruOfRp/S8cFyd9JfFQhNF0T9lz1OMvA==
|
||||
dependencies:
|
||||
date-fns "^2.29.1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user