From cc47ccaa2c6dbacb2072e4c8c859322b63c9cc02 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 21 Feb 2024 12:33:22 +0530 Subject: [PATCH] feat(ee): Add SLA management UI (#8777) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth Co-authored-by: Pranav --- app/javascript/dashboard/api/sla.js | 9 ++ .../layout/config/sidebarItems/settings.js | 10 ++ app/javascript/dashboard/featureFlags.js | 1 + .../helper/AnalyticsHelper/events.js | 6 + .../dashboard/i18n/locale/en/index.js | 2 + .../dashboard/i18n/locale/en/settings.json | 1 + .../dashboard/i18n/locale/en/sla.json | 63 ++++++++ .../dashboard/settings/settings.routes.js | 2 + .../routes/dashboard/settings/sla/AddSLA.vue | 50 +++++++ .../routes/dashboard/settings/sla/EditSLA.vue | 60 ++++++++ .../routes/dashboard/settings/sla/Index.vue | 135 +++++++++++++++++ .../routes/dashboard/settings/sla/SlaForm.vue | 141 ++++++++++++++++++ .../dashboard/settings/sla/sla.routes.js | 32 ++++ .../sla/specs/validationMixin.spec.js | 65 ++++++++ .../dashboard/settings/sla/validationMixin.js | 15 ++ .../dashboard/settings/sla/validations.js | 8 + app/javascript/dashboard/store/index.js | 2 + app/javascript/dashboard/store/modules/sla.js | 86 +++++++++++ .../dashboard/store/mutation-types.js | 7 + config/features.yml | 3 + enterprise/config/premium_features.yml | 1 + 21 files changed, 699 insertions(+) create mode 100644 app/javascript/dashboard/api/sla.js create mode 100644 app/javascript/dashboard/i18n/locale/en/sla.json create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/validations.js create mode 100644 app/javascript/dashboard/store/modules/sla.js diff --git a/app/javascript/dashboard/api/sla.js b/app/javascript/dashboard/api/sla.js new file mode 100644 index 000000000..8480b1846 --- /dev/null +++ b/app/javascript/dashboard/api/sla.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class SlaAPI extends ApiClient { + constructor() { + super('sla_policies', { accountScoped: true }); + } +} + +export default new SlaAPI(); diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 75f09e9da..444285657 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -39,6 +39,7 @@ const settings = accountId => ({ 'settings_teams_finish', 'settings_teams_list', 'settings_teams_new', + 'sla_list', ], menuItems: [ { @@ -158,6 +159,15 @@ const settings = accountId => ({ featureFlag: FEATURE_FLAGS.AUDIT_LOGS, beta: true, }, + { + icon: 'key', + label: 'SLA', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/sla/list`), + toStateName: 'sla_list', + featureFlag: FEATURE_FLAGS.SLA, + beta: true, + }, ], }); diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 9d06f15c6..2936d22ea 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -18,4 +18,5 @@ export const FEATURE_FLAGS = { AUDIT_LOGS: 'audit_logs', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', INBOX_VIEW: 'inbox_view', + SLA: 'sla', }; diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index 59361e446..4b378c74a 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -111,3 +111,9 @@ export const INBOX_EVENTS = Object.freeze({ DELETE_NOTIFICATION: 'Deleted notification', DELETE_ALL_NOTIFICATIONS: 'Deleted all notifications', }); + +export const SLA_EVENTS = Object.freeze({ + CREATE: 'Created an SLA', + UPDATE: 'Updated an SLA', + DELETED: 'Deleted an SLA', +}); diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 6cc73c5b6..75cdb5836 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -29,6 +29,7 @@ import settings from './settings.json'; import signup from './signup.json'; import teamsSettings from './teamsSettings.json'; import whatsappTemplates from './whatsappTemplates.json'; +import sla from './sla.json'; import inbox from './inbox.json'; export default { @@ -61,6 +62,7 @@ export default { ...setNewPassword, ...settings, ...signup, + ...sla, ...teamsSettings, ...whatsappTemplates, ...inbox, diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 6c4d8cbfe..d1d16a607 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -238,6 +238,7 @@ "REPORTS_INBOX": "Inbox", "REPORTS_TEAM": "Team", "SET_AVAILABILITY_TITLE": "Set yourself as", + "SLA": "SLA", "BETA": "Beta", "REPORTS_OVERVIEW": "Overview", "FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services", diff --git a/app/javascript/dashboard/i18n/locale/en/sla.json b/app/javascript/dashboard/i18n/locale/en/sla.json new file mode 100644 index 000000000..67ec4549f --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/sla.json @@ -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": "

SLA

Think of Service Level Agreements (SLAs) like friendly promises between a service provider and a customer.

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!

", + "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" + } + } + } +} diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 48ef316ef..98b81f47c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -16,6 +16,7 @@ import macros from './macros/macros.routes'; import profile from './profile/profile.routes'; import reports from './reports/reports.routes'; import store from '../../../store'; +import sla from './sla/sla.routes'; import teams from './teams/teams.routes'; export default { @@ -47,6 +48,7 @@ export default { ...macros.routes, ...profile.routes, ...reports.routes, + ...sla.routes, ...teams.routes, ], }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue new file mode 100644 index 000000000..772ec4bcd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue new file mode 100644 index 000000000..2fc577bad --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue new file mode 100644 index 000000000..473583f6d --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue @@ -0,0 +1,135 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue new file mode 100644 index 000000000..775c0ff2a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue @@ -0,0 +1,141 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js new file mode 100644 index 000000000..360c5a1f5 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js @@ -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, + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js new file mode 100644 index 000000000..a828c8556 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js @@ -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') + ); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js new file mode 100644 index 000000000..28f7436c8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js @@ -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; + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js new file mode 100644 index 000000000..bda4b9556 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js @@ -0,0 +1,8 @@ +import { required, minLength } from 'vuelidate/lib/validators'; + +export default { + name: { + required, + minLength: minLength(2), + }, +}; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 2d1564b79..91b84ddd1 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -38,6 +38,7 @@ import macros from './modules/macros'; import notifications from './modules/notifications'; import portals from './modules/helpCenterPortals'; import reports from './modules/reports'; +import sla from './modules/sla'; import teamMembers from './modules/teamMembers'; import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; @@ -109,6 +110,7 @@ export default new Vuex.Store({ userNotificationSettings, webhooks, draftMessages, + sla, }, plugins, }); diff --git a/app/javascript/dashboard/store/modules/sla.js b/app/javascript/dashboard/store/modules/sla.js new file mode 100644 index 000000000..e1ded7f51 --- /dev/null +++ b/app/javascript/dashboard/store/modules/sla.js @@ -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, +}; diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 0d4f0c010..c9d2d1767 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -301,4 +301,11 @@ export default { SET_AUDIT_LOGS_UI_FLAG: 'SET_AUDIT_LOGS_UI_FLAG', SET_AUDIT_LOGS: 'SET_AUDIT_LOGS', 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', }; diff --git a/config/features.yml b/config/features.yml index 529ad37d9..f0477c297 100644 --- a/config/features.yml +++ b/config/features.yml @@ -66,3 +66,6 @@ enabled: false - name: inbox_view enabled: false +- name: sla + enabled: false + premium: true diff --git a/enterprise/config/premium_features.yml b/enterprise/config/premium_features.yml index 9628e1da4..18e5b15ba 100644 --- a/enterprise/config/premium_features.yml +++ b/enterprise/config/premium_features.yml @@ -2,3 +2,4 @@ - disable_branding - audit_logs - response_bot +- sla