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
changes/34/22634/9
raxelsen 7 years ago
parent 6c303ccc55
commit 9823d6cc45

@ -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);
});
});
}

@ -0,0 +1,52 @@
<template>
<q-input
clearable
type="text"
:float-label="label"
:value="destination"
@input="inputDestination"
@keyup.enter="submit"
@keypress.space.prevent
@keydown.space.prevent
/>
</template>
<script>
import {
normalizeNumber,
rawNumber
} from '../../filters/number-format'
import {
QInput,
Platform
} from 'quasar-framework'
export default {
name: 'csc-destination-input',
props: {
loading: Boolean,
label: String
},
data () {
return {
destination: ''
}
},
components: {
QInput
},
methods: {
submit() {
this.$emit('submit');
},
inputDestination(value) {
this.destination = normalizeNumber(value, Platform.is.mobile);
this.$emit('input', rawNumber(this.destination));
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -69,9 +69,9 @@
<script>
import { mapGetters } from 'vuex'
import CscPage from '../../CscPage'
import CscPbxGroup from './CscPbxGroup'
import CscPbxGroupAddForm from './CscPbxGroupAddForm'
import CscPage from '../../CscPage'
import CscPbxGroup from './CscPbxGroup'
import CscPbxGroupAddForm from './CscPbxGroupAddForm'
import aliasNumberOptions from '../../../mixins/alias-number-options'
import itemError from '../../../mixins/item-error'
import { showToast } from '../../../helpers/ui'

@ -0,0 +1,160 @@
<template>
<div class="row justify-center">
<div v-if="formEnabled" class="col col-md-6 col-sm-12">
<q-field>
<q-select
:disabled="loading"
:readonly="loading"
v-model="slot"
:float-label="$t('speedDial.slot')"
:options="slotOptions"
radio
/>
</q-field>
<q-field>
<csc-destination-input
:loading="loading"
:label="$t('speedDial.destination')"
v-model="destination"
@submit="save()"
/>
</q-field>
<div
class="row justify-center form-actions"
>
<q-btn
v-if="!loading"
flat
color="secondary"
icon="clear"
@click="cancel()"
>
{{ $t('buttons.cancel') }}
</q-btn>
<q-btn
v-if="!loading"
flat
color="primary"
icon="done"
@click="save()"
>
{{ $t('buttons.save') }}
</q-btn>
</div>
</div>
<div
v-else
class="row justify-center"
>
<q-btn
color="primary"
icon="add"
flat
@click="enableForm()"
>
{{ $t('speedDial.addSpeedDial') }}
</q-btn>
</div>
<q-inner-loading
v-show="loading"
:visible="loading"
>
<q-spinner-mat
size="60px"
color="primary"
/>
</q-inner-loading>
</div>
</template>
<script>
import 'quasar-extras/animate/bounceInRight.css'
import 'quasar-extras/animate/bounceOutRight.css'
import CscDestinationInput from '../../form/CscDestinationInput'
import {
QCard,
QCardTitle,
QCardMain,
QCardActions,
QCardSeparator,
QBtn,
QInnerLoading,
QSpinnerMat,
QField,
QInput,
QSelect,
QIcon,
Alert
} from 'quasar-framework'
export default {
name: 'csc-speed-dial-add-form',
props: [
'slotOptions',
'loading'
],
data () {
return {
formEnabled: false,
destination: '',
slot: ''
}
},
components: {
CscDestinationInput,
QCard,
QCardTitle,
QCardMain,
QCardActions,
QCardSeparator,
QBtn,
QInnerLoading,
QSpinnerMat,
QField,
QInput,
QSelect,
QIcon
},
methods: {
destinationInput(input) {
this.destination = input;
},
enableForm(){
if (this.slotOptions.length > 0) {
this.reset();
this.formEnabled = true;
}
else {
Alert.create({
enter: 'bounceInRight',
leave: 'bounceOutRight',
position: 'top-center',
html: this.$t('speedDial.addNoSlotsDialogText'),
icon: 'warning',
dismissible: true
});
}
},
cancel() {
this.formEnabled = false;
},
save() {
this.$emit('save', {
destination: this.destination,
slot: this.slot
});
},
reset() {
this.destination = '';
this.slot = this.slotOptions[0].value ? this.slotOptions[0].value : '';
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../../themes/quasar.variables.styl';
.form-actions
margin-top 16px
margin-bottom 8px
</style>

@ -2,10 +2,20 @@
<csc-page class="csc-list-page">
<q-list
no-border
inset-separator
separator
sparse
multiline
>
<q-item>
<q-item-main>
<csc-speed-dial-add-form
ref="addForm"
@save="assignSpeedDial"
:loading="isAdding"
:slot-options="unassignedSlots"
/>
</q-item-main>
</q-item>
<q-item
v-for="(assigned, index) in assignedSlots"
:key="index"
@ -53,13 +63,14 @@
<script>
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
}));
}
}
}
}

@ -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}"
}
}

@ -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'

@ -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);
});
}
}
};

@ -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);
});
});
});

@ -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);
});
});
Loading…
Cancel
Save