From 9823d6cc45f23ae0a785734fd44fcf9b2b292e94 Mon Sep 17 00:00:00 2001 From: raxelsen Date: Mon, 23 Jul 2018 16:10:25 +0200 Subject: [PATCH] TT#40255 Customer wants to add speed dial config What has been done: - TT#40529, Implement API method for assigning a destination to a specific speed dial slot - TT#40630, Implement API method for fetching available slots - TT#40530, Implement reload of list after new assignment has been created successfully - TT#40527, Implement UI form and buttons (destination input field, slot selection field, and add button) - TT#40528, Implement custom phone number input field with automated formatting - TT#40531, Implement toast for successfully assigned destination - TT#40538, Implement unit test for creation of selections options for speed dial slot selection Change-Id: Ibe4937a097be0a89e677916a189a0e3fe62826df --- src/api/speed-dial.js | 49 +++++- src/components/form/CscDestinationInput.vue | 52 ++++++ .../pages/PbxConfiguration/CscPbxGroups.vue | 6 +- .../pages/SpeedDial/CscSpeedDialAddForm.vue | 160 ++++++++++++++++++ .../pages/{ => SpeedDial}/SpeedDial.vue | 51 +++++- src/locales/en.json | 8 +- src/routes.js | 2 +- src/store/speed-dial.js | 70 +++++++- t/api/speed-dial.js | 114 ++++++++++++- t/store/speed-dial.js | 27 +++ 10 files changed, 504 insertions(+), 35 deletions(-) create mode 100644 src/components/form/CscDestinationInput.vue create mode 100644 src/components/pages/SpeedDial/CscSpeedDialAddForm.vue rename src/components/pages/{ => SpeedDial}/SpeedDial.vue (73%) create mode 100644 t/store/speed-dial.js diff --git a/src/api/speed-dial.js b/src/api/speed-dial.js index 837a9c2d..9e3e7ddf 100644 --- a/src/api/speed-dial.js +++ b/src/api/speed-dial.js @@ -1,9 +1,10 @@ import _ from 'lodash' import Vue from 'vue'; +import { i18n } from '../i18n'; import { getFieldList } from './common' -export function getSpeedDials(id) { +export function getSpeedDialsById(id) { return new Promise((resolve, reject) => { getFieldList({ path: 'api/speeddials/' + id, @@ -21,15 +22,19 @@ export function getUnassignedSlots(id) { return new Promise((resolve, reject) => { let slots = ["*0", "*1", "*2", "*3", "*4", "*5", "*6", "*7", "*8", "*9"]; Promise.resolve().then(() => { - return getSpeedDials(id); + return getSpeedDialsById(id); }).then((assignedSlots) => { - // TODO: Split into own testable function that takes slots and - // unassigned slots, and outputs slotOptions array ready to be - // consumed by q-select let unassignedSlots = _.difference(slots, assignedSlots.map((slot) => { return slot.slot; })); - resolve(unassignedSlots); + let slotOptions = []; + unassignedSlots.forEach((slot) => { + slotOptions.push({ + label: `${i18n.t('speedDial.slot')} ${slot}`, + value: slot + }); + }); + resolve(slotOptions); }).catch((err) => { reject(err.body.message); }); @@ -53,3 +58,35 @@ export function unassignSpeedDialSlot(options) { }); }); } + +export function addSlotToSpeedDials(options) { + return new Promise((resolve, reject) => { + let headers = { + 'Content-Type': 'application/json-patch+json' + }; + Vue.http.patch('api/speeddials/' + options.id, [{ + op: 'replace', + path: '/speeddials', + value: options.slots + }], { headers: headers }).then(() => { + resolve(); + }).catch((err) => { + reject(err.body.message); + }); + }); +} + +export function assignSpeedDialSlot(options) { + return new Promise((resolve, reject) => { + Promise.resolve().then(() => { + return getSpeedDialsById(options.id); + }).then((result) => { + let concatSlots = result.concat(options.slot); + return addSlotToSpeedDials({ id: options.id, slots: concatSlots }); + }).then(() => { + resolve(); + }).catch((err) => { + reject(err); + }); + }); +} diff --git a/src/components/form/CscDestinationInput.vue b/src/components/form/CscDestinationInput.vue new file mode 100644 index 00000000..fa6b39fb --- /dev/null +++ b/src/components/form/CscDestinationInput.vue @@ -0,0 +1,52 @@ + + + + + + diff --git a/src/components/pages/PbxConfiguration/CscPbxGroups.vue b/src/components/pages/PbxConfiguration/CscPbxGroups.vue index 679950f7..c5a3d816 100644 --- a/src/components/pages/PbxConfiguration/CscPbxGroups.vue +++ b/src/components/pages/PbxConfiguration/CscPbxGroups.vue @@ -69,9 +69,9 @@ + + diff --git a/src/components/pages/SpeedDial.vue b/src/components/pages/SpeedDial/SpeedDial.vue similarity index 73% rename from src/components/pages/SpeedDial.vue rename to src/components/pages/SpeedDial/SpeedDial.vue index 4385acd7..bad9bcdf 100644 --- a/src/components/pages/SpeedDial.vue +++ b/src/components/pages/SpeedDial/SpeedDial.vue @@ -2,10 +2,20 @@ + + + + + import { mapGetters } from 'vuex' + import CscPage from '../../CscPage' + import CscSpeedDialAddForm from './CscSpeedDialAddForm' import { startLoading, stopLoading, showToast, showGlobalError - } from '../../helpers/ui' - import CscPage from '../CscPage' + } from '../../../helpers/ui' import { QList, QItem, @@ -72,8 +83,14 @@ } from 'quasar-framework' export default { + data () { + return { + addFormEnabled: true + } + }, components: { CscPage, + CscSpeedDialAddForm, QList, QItem, QItemMain, @@ -84,6 +101,7 @@ }, created() { this.$store.dispatch('speedDial/loadSpeedDials'); + this.$store.dispatch('speedDial/getUnassignedSlots'); }, computed: { ...mapGetters('speedDial', [ @@ -92,17 +110,25 @@ 'speedDialLoadingError', 'unassignSlotState', 'unassignSlotError', - 'lastUnassignedSlot' + 'lastUnassignedSlot', + 'unassignedSlots', + 'assignSlotState', + 'assignSlotError', + 'lastAssignedSlot', + 'isAdding' ]) }, methods: { - unassignSlot(slot) { + assignSpeedDial(assigned) { + this.$store.dispatch('speedDial/assignSpeedDialSlot', assigned); + }, + unassignSlot(unassigned) { let self = this; let store = this.$store; Dialog.create({ title: self.$t('speedDial.removeDialogTitle'), message: self.$t('speedDial.removeDialogText', { - slot: slot.slot + slot: unassigned.slot }), buttons: [ self.$t('buttons.cancel'), @@ -110,7 +136,7 @@ label: self.$t('buttons.remove'), color: 'negative', handler () { - store.dispatch('speedDial/unassignSpeedDialSlot', slot) + store.dispatch('speedDial/unassignSpeedDialSlot', unassigned) } } ] @@ -144,6 +170,17 @@ slot: this.lastUnassignedSlot })); } + }, + assignSlotState(state) { + if (state === 'failed') { + showGlobalError(this.assignSlotError); + } + else if (state === 'succeeded') { + this.$refs.addForm.cancel(); + showToast(this.$t('speedDial.assignSlotSuccessMessage', { + slot: this.lastAssignedSlot + })); + } } } } diff --git a/src/locales/en.json b/src/locales/en.json index 04a0796f..0ffa97cc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -372,6 +372,12 @@ "removeDialogTitle": "Remove speed dial", "removeDialogText": "You are about to remove the speed dial {slot}", "unassignSlotErrorMessage": "An error occured while trying to unassign the speed dial slot. Please try again.", - "unassignSlotSuccessMessage": "Unassigned slot {slot}" + "unassignSlotSuccessMessage": "Unassigned slot {slot}", + "addSpeedDial": "Add Speed Dial", + "slot": "Slot", + "destination": "Destination", + "addNoSlotsDialogText": "All available speed dial slots have already been assigned. Please delete one first.", + "assignSlotErrorMessage": "An error occured while trying to assign the speed dial slot. Please try again.", + "assignSlotSuccessMessage": "Assigned slot {slot}" } } diff --git a/src/routes.js b/src/routes.js index 5e830b8e..39bb2848 100644 --- a/src/routes.js +++ b/src/routes.js @@ -10,7 +10,7 @@ import CallBlockingIncoming from './components/pages/CallBlocking/Incoming' import CallBlockingOutgoing from './components/pages/CallBlocking/Outgoing' import CallBlockingPrivacy from './components/pages/CallBlocking/Privacy' import Reminder from './components/pages/Reminder'; -import SpeedDial from './components/pages/SpeedDial' +import SpeedDial from './components/pages/SpeedDial/SpeedDial' import PbxConfigurationGroups from './components/pages/PbxConfiguration/CscPbxGroups' import PbxConfigurationSeats from './components/pages/PbxConfiguration/CscPbxSeats' import PbxConfigurationDevices from './components/pages/PbxConfiguration/CscPbxDevices' diff --git a/src/store/speed-dial.js b/src/store/speed-dial.js index 9db97cba..77f99529 100644 --- a/src/store/speed-dial.js +++ b/src/store/speed-dial.js @@ -3,20 +3,25 @@ import { i18n } from '../i18n'; import { RequestState } from './common' import { - getSpeedDials, - unassignSpeedDialSlot + getSpeedDialsById, + unassignSpeedDialSlot, + getUnassignedSlots, + assignSpeedDialSlot } from '../api/speed-dial'; export default { namespaced: true, state: { assignedSlots: [], - slotOptions: [], speedDialLoadingState: RequestState.initiated, speedDialError: null, unassignSlotState: RequestState.initiated, unassignSlotError: null, - lastUnassignedSlot: null + lastUnassignedSlot: null, + unassignedSlots: [], + assignSlotState: RequestState.initiated, + assignSlotError: null, + lastAssignedSlot: null }, getters: { reminderLoadingState(state) { @@ -45,6 +50,21 @@ export default { }, lastUnassignedSlot(state) { return state.lastUnassignedSlot; + }, + unassignedSlots(state) { + return state.unassignedSlots; + }, + assignSlotState(state) { + return state.assignSlotState; + }, + assignSlotError(state) { + return state.assignSlotError || i18n.t('speedDial.assignSlotErrorMessage'); + }, + lastAssignedSlot(state) { + return state.lastAssignedSlot; + }, + isAdding(state) { + return state.assignSlotState === RequestState.requesting; } }, mutations: { @@ -73,29 +93,63 @@ export default { unassignSlotFailed(state, error) { state.unassignSlotState = RequestState.failed; state.unassignSlotError = error; + }, + loadUnassignedSlots(state, result) { + state.unassignedSlots = result; + }, + assignSlotRequesting(state) { + state.assignSlotState = RequestState.requesting; + state.assignSlotError = null; + }, + assignSlotSucceeded(state, last) { + state.lastAssignedSlot = last; + state.assignSlotState = RequestState.succeeded; + state.assignSlotError = null; + }, + assignSlotFailed(state, error) { + state.assignSlotState = RequestState.failed; + state.assignSlotError = error; } }, actions: { loadSpeedDials(context) { context.commit('speedDialRequesting'); - getSpeedDials(context.getters.subscriberId).then((slots) => { + getSpeedDialsById(context.getters.subscriberId).then((slots) => { context.commit('speedDialSucceeded', slots); }).catch((error) => { context.commit('speedDialFailed', error); }); }, - unassignSpeedDialSlot(context, slot) { + unassignSpeedDialSlot(context, unassigned) { context.commit('unassignSlotRequesting'); unassignSpeedDialSlot({ slots: context.state.assignedSlots, - slot: slot, + slot: unassigned, id: context.getters.subscriberId }).then(() => { - context.commit('unassignSlotSucceeded', slot.slot); + context.commit('unassignSlotSucceeded', unassigned.slot); context.dispatch('loadSpeedDials'); }).catch((error) => { context.commit('unassignSlotFailed', error); }); + }, + getUnassignedSlots(context) { + getUnassignedSlots(context.getters.subscriberId).then((result) => { + context.commit('loadUnassignedSlots', result); + }); + }, + assignSpeedDialSlot(context, assigned) { + context.commit('assignSlotRequesting'); + assignSpeedDialSlot({ + id: context.getters.subscriberId, + slot: assigned + }).then(() => { + context.commit('assignSlotSucceeded', assigned.slot); + context.dispatch('loadSpeedDials'); + context.dispatch('getUnassignedSlots'); + }).catch((error) => { + context.commit('assignSlotFailed', error); + }); } } }; diff --git a/t/api/speed-dial.js b/t/api/speed-dial.js index b2843e37..c6f96d12 100644 --- a/t/api/speed-dial.js +++ b/t/api/speed-dial.js @@ -7,13 +7,14 @@ import { getFieldList } from '../../src/api/common'; import { - getSpeedDials + getSpeedDialsById, + getUnassignedSlots } from '../../src/api/speed-dial'; import { assert } from 'chai'; Vue.use(VueResource); -describe('Speed Dials', function(){ +describe('SpeedDial', function(){ const subscriberId = 123; @@ -53,15 +54,15 @@ describe('Speed Dials', function(){ }, "speeddials" : [ { - "destination" : "sip:439965050@10.15.17.240", + "destination" : "sip:439965050@192.168.178.23", "slot" : "*9" }, { - "destination" : "sip:22222222@10.15.17.240", + "destination" : "sip:22222222@192.168.178.23", "slot" : "*0" }, { - "destination" : "sip:43665522@10.15.17.240", + "destination" : "sip:43665522@192.168.178.23", "slot" : "*3" } ] @@ -69,15 +70,15 @@ describe('Speed Dials', function(){ let fieldList = [ { - "destination" : "sip:22222222@10.15.17.240", + "destination" : "sip:22222222@192.168.178.23", "slot" : "*0" }, { - "destination" : "sip:43665522@10.15.17.240", + "destination" : "sip:43665522@192.168.178.23", "slot" : "*3" }, { - "destination" : "sip:439965050@10.15.17.240", + "destination" : "sip:439965050@192.168.178.23", "slot" : "*9" } ]; @@ -88,7 +89,7 @@ describe('Speed Dials', function(){ status: 200 })); }); - getSpeedDials(subscriberId).then((result)=>{ + getSpeedDialsById(subscriberId).then((result)=>{ assert.deepEqual(result, fieldList); done(); }).catch((err)=>{ @@ -96,4 +97,99 @@ describe('Speed Dials', function(){ }); }); + it('should get list of unassigned speed dial slots', function(done){ + + let data = { + "_links" : { + "collection" : { + "href" : "/api/speeddials/" + }, + "curies" : { + "href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}", + "name" : "ngcp", + "templated" : true + }, + "ngcp:journal" : [ + { + "href" : "/api/speeddials/323/journal/" + } + ], + "ngcp:speeddials" : [ + { + "href" : "/api/speeddials/323" + } + ], + "ngcp:subscribers" : [ + { + "href" : "/api/subscribers/323" + } + ], + "profile" : { + "href" : "http://purl.org/sipwise/ngcp-api/" + }, + "self" : { + "href" : "/api/speeddials/323" + } + }, + "speeddials" : [ + { + "destination" : "sip:439965050@192.168.178.23", + "slot" : "*9" + }, + { + "destination" : "sip:22222222@192.168.178.23", + "slot" : "*0" + }, + { + "destination" : "sip:43665522@192.168.178.23", + "slot" : "*3" + } + ] + }; + + let slotOptions = [ + { + "label" : "Slot *1", + "value" : "*1" + }, + { + "label" : "Slot *2", + "value" : "*2" + }, + { + "label" : "Slot *4", + "value" : "*4" + }, + { + "label" : "Slot *5", + "value" : "*5" + }, + { + "label" : "Slot *6", + "value" : "*6" + }, + { + "label" : "Slot *7", + "value" : "*7" + }, + { + "label" : "Slot *8", + "value" : "*8" + } + ]; + + Vue.http.interceptors = []; + Vue.http.interceptors.unshift((request, next)=>{ + next(request.respondWith(JSON.stringify(data), { + status: 200 + })); + }); + getUnassignedSlots(subscriberId).then((result)=>{ + assert.deepEqual(result, slotOptions); + done(); + }).catch((err)=>{ + done(err); + }); + }); + }); diff --git a/t/store/speed-dial.js b/t/store/speed-dial.js new file mode 100644 index 00000000..ead01284 --- /dev/null +++ b/t/store/speed-dial.js @@ -0,0 +1,27 @@ + +'use strict'; + +import SpeedDialModule from '../../src/store/speed-dial'; +import { assert } from 'chai'; + +describe('SpeedDial', function(){ + + it('should load all assigned speed dial slots', function(){ + let state = { + assignedSlots: [] + }; + let data = [ + { + destination: "sip:111111@192.168.178.23", + slot: "*1" + }, + { + destination: "sip:333333@192.168.178.23", + slot: "*3" + } + ]; + SpeedDialModule.mutations.speedDialSucceeded(state, data); + assert.deepEqual(state.assignedSlots, data); + }); + +});