feat: init sla ui

This commit is contained in:
Vishnu Narayanan
2024-01-24 19:23:55 +05:30
parent a861257f73
commit b36dfb79db
8 changed files with 596 additions and 0 deletions
@@ -0,0 +1,122 @@
<template>
<div class="h-auto overflow-auto flex flex-col">
<woot-modal-header
:header-title="$t('SLA_MGMT.ADD.TITLE')"
:header-content="$t('SLA_MGMT.ADD.DESC')"
/>
<form class="mx-0 flex flex-wrap" @submit.prevent="addSLA">
<woot-input
v-model.trim="title"
:class="{ error: $v.title.$error }"
class="w-full sla-name--input"
:sla="$t('SLA_MGMT.FORM.NAME.SLA')"
:placeholder="$t('SLA_MGMT.FORM.NAME.PLACEHOLDER')"
:error="getSLATitleErrorMessage"
data-testid="sla-title"
@input="$v.title.$touch"
/>
<woot-input
v-model.trim="description"
:class="{ error: $v.description.$error }"
class="w-full"
:sla="$t('SLA_MGMT.FORM.DESCRIPTION.SLA')"
:placeholder="$t('SLA_MGMT.FORM.DESCRIPTION.PLACEHOLDER')"
data-testid="sla-description"
@input="$v.description.$touch"
/>
<div class="w-full">
<sla>
{{ $t('SLA_MGMT.FORM.COLOR.SLA') }}
<woot-color-picker v-model="color" />
</sla>
</div>
<div class="w-full">
<input v-model="showOnSidebar" type="checkbox" :value="true" />
<sla for="conversation_creation">
{{ $t('SLA_MGMT.FORM.SHOW_ON_SIDEBAR.SLA') }}
</sla>
</div>
<div class="flex justify-end items-center py-2 px-0 gap-2 w-full">
<woot-button
:is-disabled="$v.title.$invalid || uiFlags.isCreating"
:is-loading="uiFlags.isCreating"
data-testid="sla-submit"
>
{{ $t('SLA_MGMT.FORM.CREATE') }}
</woot-button>
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('SLA_MGMT.FORM.CANCEL') }}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import validationMixin from './validationMixin';
import { mapGetters } from 'vuex';
import validations from './validations';
import { getRandomColor } from 'dashboard/helper/slaColor';
export default {
mixins: [alertMixin, validationMixin],
props: {
prefillTitle: {
type: String,
default: '',
},
},
data() {
return {
color: '#000',
description: '',
title: '',
showOnSidebar: true,
};
},
validations,
computed: {
...mapGetters({
uiFlags: 'slas/getUIFlags',
}),
},
mounted() {
this.color = getRandomColor();
this.title = this.prefillTitle.toLowerCase();
},
methods: {
onClose() {
this.$emit('close');
},
async addSLA() {
try {
await this.$store.dispatch('slas/create', {
color: this.color,
description: this.description,
title: this.title.toLowerCase(),
show_on_sidebar: this.showOnSidebar,
});
this.showAlert(this.$t('SLA_MGMT.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
const errorMessage =
error.message || this.$t('SLA_MGMT.ADD.API.ERROR_MESSAGE');
this.showAlert(errorMessage);
}
},
},
};
</script>
<style lang="scss" scoped>
// SLA API supports only lowercase letters
.sla-name--input {
::v-deep {
input {
@apply lowercase;
}
}
}
</style>
@@ -0,0 +1,125 @@
<template>
<div class="h-auto overflow-auto flex flex-col">
<woot-modal-header :header-title="pageTitle" />
<form class="mx-0 flex flex-wrap" @submit.prevent="editSLA">
<woot-input
v-model.trim="title"
:class="{ error: $v.title.$error }"
class="w-full sla-name--input"
:sla="$t('SLA_MGMT.FORM.NAME.SLA')"
:placeholder="$t('SLA_MGMT.FORM.NAME.PLACEHOLDER')"
:error="getSLATitleErrorMessage"
@input="$v.title.$touch"
/>
<woot-input
v-model.trim="description"
:class="{ error: $v.description.$error }"
class="w-full"
:sla="$t('SLA_MGMT.FORM.DESCRIPTION.SLA')"
:placeholder="$t('SLA_MGMT.FORM.DESCRIPTION.PLACEHOLDER')"
@input="$v.description.$touch"
/>
<div class="w-full">
<sla>
{{ $t('SLA_MGMT.FORM.COLOR.SLA') }}
<woot-color-picker v-model="color" />
</sla>
</div>
<div class="w-full">
<input v-model="showOnSidebar" type="checkbox" :value="true" />
<sla for="conversation_creation">
{{ $t('SLA_MGMT.FORM.SHOW_ON_SIDEBAR.SLA') }}
</sla>
</div>
<div class="flex justify-end items-center py-2 px-0 gap-2 w-full">
<woot-button
:is-disabled="$v.title.$invalid || uiFlags.isUpdating"
:is-loading="uiFlags.isUpdating"
>
{{ $t('SLA_MGMT.FORM.EDIT') }}
</woot-button>
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('SLA_MGMT.FORM.CANCEL') }}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import validationMixin from './validationMixin';
import validations from './validations';
export default {
mixins: [alertMixin, validationMixin],
props: {
selectedResponse: {
type: Object,
default: () => {},
},
},
data() {
return {
title: '',
description: '',
showOnSidebar: true,
color: '',
};
},
validations,
computed: {
...mapGetters({
uiFlags: 'slas/getUIFlags',
}),
pageTitle() {
return `${this.$t('SLA_MGMT.EDIT.TITLE')} - ${
this.selectedResponse.title
}`;
},
},
mounted() {
this.setFormValues();
},
methods: {
onClose() {
this.$emit('close');
},
setFormValues() {
this.title = this.selectedResponse.title;
this.description = this.selectedResponse.description;
this.showOnSidebar = this.selectedResponse.show_on_sidebar;
this.color = this.selectedResponse.color;
},
editSLA() {
this.$store
.dispatch('slas/update', {
id: this.selectedResponse.id,
color: this.color,
description: this.description,
title: this.title.toLowerCase(),
show_on_sidebar: this.showOnSidebar,
})
.then(() => {
this.showAlert(this.$t('SLA_MGMT.EDIT.API.SUCCESS_MESSAGE'));
setTimeout(() => this.onClose(), 10);
})
.catch(() => {
this.showAlert(this.$t('SLA_MGMT.EDIT.API.ERROR_MESSAGE'));
});
},
},
};
</script>
<style lang="scss" scoped>
// SLA API supports only lowercase letters
.sla-name--input {
::v-deep {
input {
@apply lowercase;
}
}
}
</style>
@@ -0,0 +1,202 @@
<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_MGMT.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_MGMT.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('SLA_MGMT.LOADING')"
/>
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
<thead>
<th
v-for="thHeader in $t('SLA_MGMT.LIST.TABLE_HEADER')"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr v-for="(sla, index) in records" :key="sla.title">
<td class="sla-title">
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
sla.title
}}</span>
</td>
<td>{{ sla.description }}</td>
<td>
<div class="sla-color--container">
<span
class="sla-color--display"
:style="{ backgroundColor: sla.color }"
/>
{{ sla.color }}
</div>
</td>
<td class="button-wrapper">
<woot-button
v-tooltip.top="$t('SLA_MGMT.FORM.EDIT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class-names="grey-btn"
:is-loading="loading[sla.id]"
icon="edit"
@click="openEditPopup(sla)"
/>
<woot-button
v-tooltip.top="$t('SLA_MGMT.FORM.DELETE')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
class-names="grey-btn"
:is-loading="loading[sla.id]"
@click="openDeletePopup(sla, index)"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="w-[34%]">
<span v-dompurify-html="$t('SLA_MGMT.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>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('SLA_MGMT.DELETE.CONFIRM.TITLE')"
:message="$t('SLA_MGMT.DELETE.CONFIRM.MESSAGE')"
:message-value="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</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,
showDeleteConfirmationPopup: false,
selectedResponse: {},
};
},
computed: {
...mapGetters({
records: 'slas/getslas',
uiFlags: 'slas/getUIFlags',
}),
// Delete Modal
deleteConfirmText() {
return this.$t('SLA_MGMT.DELETE.CONFIRM.YES');
},
deleteRejectText() {
return this.$t('SLA_MGMT.DELETE.CONFIRM.NO');
},
deleteMessage() {
return ` ${this.selectedResponse.title}?`;
},
},
mounted() {
this.$store.dispatch('slas/get');
},
methods: {
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
openEditPopup(response) {
this.showEditPopup = true;
this.selectedResponse = response;
},
hideEditPopup() {
this.showEditPopup = false;
},
openDeletePopup(response) {
this.showDeleteConfirmationPopup = true;
this.selectedResponse = response;
},
closeDeletePopup() {
this.showDeleteConfirmationPopup = false;
},
confirmDeletion() {
this.loading[this.selectedResponse.id] = true;
this.closeDeletePopup();
this.deletesla(this.selectedResponse.id);
},
deletesla(id) {
this.$store
.dispatch('slas/delete', id)
.then(() => {
this.showAlert(this.$t('SLA_MGMT.DELETE.API.SUCCESS_MESSAGE'));
})
.catch(() => {
this.showAlert(this.$t('SLA_MGMT.DELETE.API.ERROR_MESSAGE'));
})
.finally(() => {
this.loading[this.selectedResponse.id] = false;
});
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
.sla-color--container {
@apply flex items-center;
}
.sla-color--display {
@apply rounded h-4 w-4 mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-50 dark:border-slate-700;
}
.sla-title {
span {
@apply w-60 inline-block;
}
}
</style>
@@ -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_MGMT.HEADER',
icon: 'tag',
showNewButton: false,
},
children: [
{
path: '',
name: 'sla_wrapper',
roles: ['administrator'],
redirect: 'list',
},
{
path: 'list',
name: 'sla_list',
roles: ['administrator'],
component: Index,
},
],
},
],
};
@@ -0,0 +1,72 @@
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 Component = {
render() {},
title: 'TestComponent',
mixins: [validationMixin],
validations,
};
describe('validationMixin', () => {
it('it should return empty error message if valid label name passed', async () => {
const wrapper = shallowMount(Component, {
i18n: i18nConfig,
localVue,
data() {
return { title: 'sales' };
},
});
wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('');
});
it('it should return label required error message if empty name is passed', async () => {
const wrapper = shallowMount(Component, {
i18n: i18nConfig,
localVue,
data() {
return { title: '' };
},
});
wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('Label name is required');
});
it('it should return label minimum length error message if one charceter label name is passed', async () => {
const wrapper = shallowMount(Component, {
i18n: i18nConfig,
localVue,
data() {
return { title: 's' };
},
});
wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
'Minimum length 2 is required'
);
});
it('it should return invalid character error message if invalid lable name passed', async () => {
const wrapper = shallowMount(Component, {
i18n: i18nConfig,
localVue,
data() {
return { title: 'sales enquiry' };
},
});
wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
'Only Alphabets, Numbers, Hyphen and Underscore are allowed'
);
});
});
@@ -0,0 +1,10 @@
import { validLabelCharacters } from '../validations';
describe('#validLabelCharacters', () => {
it('validates the label', () => {
expect(validLabelCharacters('')).toEqual(false);
expect(validLabelCharacters('str str')).toEqual(false);
expect(validLabelCharacters('str_str')).toEqual(true);
expect(validLabelCharacters('str-str')).toEqual(true);
});
});
@@ -0,0 +1,17 @@
export default {
computed: {
getLabelTitleErrorMessage() {
let errorMessage = '';
if (!this.$v.title.$error) {
errorMessage = '';
} else if (!this.$v.title.required) {
errorMessage = this.$t('SLA_MGMT.FORM.NAME.REQUIRED_ERROR');
} else if (!this.$v.title.minLength) {
errorMessage = this.$t('SLA_MGMT.FORM.NAME.MINIMUM_LENGTH_ERROR');
} else if (!this.$v.title.validLabelCharacters) {
errorMessage = this.$t('SLA_MGMT.FORM.NAME.VALID_ERROR');
}
return errorMessage;
},
},
};
@@ -0,0 +1,16 @@
import { required, minLength } from 'vuelidate/lib/validators';
export const validLabelCharacters = (str = '') => !!str && !str.includes(' ');
export default {
title: {
required,
minLength: minLength(2),
validLabelCharacters,
},
description: {},
color: {
required,
},
showOnSidebar: {},
};