TT#40257 List of assigned speed dials

What has been done:
- TT#40522, Investigate and discover necessary API endpoints
- TT#40523, Implement speed dial page, navigation menu entry and home
  page box
- TT#40525, Implement UI display for list of speed dials
- TT#40524, Implement API method for fetching speed dial slots 0-9
- TT#40526, Implement loading animation and error alert
- TT#41016, Implement test API speed dial method

Change-Id: I0b6fc4dab08913948bb8acf9c136bd56fe89da01
changes/61/22561/8
raxelsen 7 years ago
parent 8e4fb6446b
commit 6117c5f6fe

@ -99,3 +99,22 @@ export function patchReplace(options) {
});
});
}
export function getFieldList(options) {
return new Promise((resolve, reject) => {
options = options || {};
options = _.merge({
headers: {
'Accept': 'application/json'
}
}, options);
Vue.http.get(options.path, {
headers: options.headers
}).then((result) => {
let fieldList = getJsonBody(result.body)[options.field];
resolve(fieldList);
}).catch((err) => {
reject(err);
});
});
}

@ -0,0 +1,36 @@
import _ from 'lodash'
import { getFieldList } from './common'
export function getSpeedDials(id) {
return new Promise((resolve, reject) => {
getFieldList({
path: 'api/speeddials/' + id,
field: 'speeddials'
}).then((result) => {
let sortedResult = _.sortBy(result, ['slot']);
resolve(sortedResult);
}).catch((err) => {
reject(err.body.message);
});
});
}
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);
}).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);
}).catch((err) => {
reject(err.body.message);
});
});
}

@ -191,8 +191,15 @@
>
<q-item-side icon="fa-bell"/>
<q-item-main
label="Reminder"
sublabel="Set your personal alarm"
:label="$t('navigation.reminder.title')"
:sublabel="$t('navigation.reminder.subTitle')"
/>
</q-side-link>
<q-side-link item to="/user/speeddial">
<q-item-side icon="touch app"/>
<q-item-main
:label="$t('navigation.speeddial.title')"
:sublabel="$t('navigation.speeddial.subTitle')"
/>
</q-side-link>
<q-collapsible

@ -1,22 +1,17 @@
<template>
<q-item
class="csc-entity csc-call-item"
>
<q-item class="csc-entity csc-call-item">
<q-item-side
:icon="icon"
:color="color"
/>
<q-item-main>
<q-item-tile
label
>
<q-item-tile label>
<span class="gt-sm csc-entity-title">{{ typeTerm }}</span>
<span class="gt-sm csc-entity-title">{{ direction }}</span>
<span class="csc-entity-title">{{ number | destinationFormat }}</span>
</q-item-tile>
<q-item-tile
sublabel
>{{ call.start_time | smartTime }}
<q-item-tile sublabel>
{{ call.start_time | smartTime }}
</q-item-tile>
</q-item-main>
<q-item-side
@ -53,7 +48,6 @@
QItemTile,
QBtn,
QPopover,
QList,
Platform
} from 'quasar-framework'
export default {
@ -69,7 +63,6 @@
QItemTile,
QBtn,
QPopover,
QList,
CscCallOptionList
},
data () {

@ -8,9 +8,15 @@
<q-item-tile
label
>
<span class="gt-sm csc-entity-title">Fax </span>
<span class="gt-sm csc-entity-title">{{ direction }}</span>
<span class="csc-entity-title">{{ fax.caller | numberFormat }}</span>
<span class="gt-sm csc-entity-title">
{{ $t('pages.conversations.fax') }}
</span>
<span class="gt-sm csc-entity-title">
{{ direction }}
</span>
<span class="csc-entity-title">
{{ fax.caller | numberFormat }}
</span>
</q-item-tile>
<q-item-tile
sublabel
@ -23,12 +29,14 @@
<q-item-tile
v-else-if="fax.pages === 1"
sublabel
>{{ fax.pages }} page
>
{{ fax.pages }} {{ $t('pages.conversations.page') }}
</q-item-tile>
<q-item-tile
v-else
sublabel
>{{ fax.pages }} pages
>
{{ fax.pages }} {{ $t('pages.conversations.pages') }}
</q-item-tile>
</q-item-main>
<q-item-side

@ -10,9 +10,15 @@
<q-item-tile
label
>
<span class="gt-sm csc-entity-title">Voicemail </span>
<span class="gt-sm csc-entity-title">{{ direction }}</span>
<span class="csc-entity-title">{{ voiceMail.caller | numberFormat }}</span>
<span class="gt-sm csc-entity-title">
{{ $t('pages.conversations.voicemail') }}
</span>
<span class="gt-sm csc-entity-title">
{{ direction }}
</span>
<span class="csc-entity-title">
{{ voiceMail.caller | numberFormat }}
</span>
</q-item-tile>
<q-item-tile
sublabel
@ -22,7 +28,9 @@
<q-item-tile
sublabel
>
Duration: {{ voiceMail.duration }} seconds
{{ t$('pages.conversations.duration') }}
{{ voiceMail.duration }}
{{ t$('pages.conversations.seconds') }}
</q-item-tile>
<q-item-tile>
<csc-voice-mail-player

@ -82,6 +82,18 @@
</q-card>
</router-link>
</div>
<div class="col-lg-4 col-md-6 col-sm-6 col-xs-6">
<router-link to="/user/speeddial">
<q-card class="home-card-active no-margin" flat>
<q-card-main align="center">
<q-icon name="touch app" class="home-icons" />
</q-card-main>
<q-card-actions align="center">
{{ $t('pages.home.cards.speeddial') }}
</q-card-actions>
</q-card>
</router-link>
</div>
<div class="col-lg-4 col-md-6 col-sm-6 col-xs-6">
<q-card @click="buddyList()" class="home-card-inactive no-margin" flat>
<q-card-main align="center">

@ -0,0 +1,116 @@
<template>
<csc-page class="csc-list-page">
<q-list
no-border
inset-separator
sparse
multiline
>
<q-item
v-for="(assigned, index) in assignedSlots"
:key="index"
class="csc-entity"
>
<q-item-side
icon="touch app"
color="primary"
/>
<q-item-main>
<q-item-tile label>
<span class="csc-entity-title">
{{ $t('speedDial.whenIDial', { slot: assigned.slot }) }}
</span>
</q-item-tile>
<q-item-tile sublabel>
{{ $t('speedDial.ring') }}
{{ assigned.destination | destinationFormat }}
</q-item-tile>
</q-item-main>
<q-item-side
right
class="csc-item-buttons"
>
<q-item-tile>
<q-btn
flat
icon="delete"
color="negative"
slot="right"
@click="deleteAssignment(index)"
/>
</q-item-tile>
</q-item-side>
</q-item>
</q-list>
<div
v-if="assignedSlots.length === 0"
class="row justify-center"
>
{{ $t('speedDial.noResultsMessage') }}
</div>
</csc-page>
</template>
<script>
import { mapGetters } from 'vuex'
import {
startLoading,
stopLoading,
showGlobalError
} from '../../helpers/ui'
import CscPage from '../CscPage'
import {
QList,
QItem,
QItemMain,
QItemTile,
QItemSide,
QChip,
QBtn
} from 'quasar-framework'
export default {
components: {
CscPage,
QList,
QItem,
QItemMain,
QItemTile,
QItemSide,
QChip,
QBtn
},
created() {
this.$store.dispatch('speedDial/loadSpeedDials');
},
computed: {
...mapGetters('speedDial', [
'assignedSlots',
'speedDialLoadingState',
'speedDialLoadingError'
])
},
methods: {
deleteAssignment(index) {
console.log('deleteAssignment(), index', index);
}
},
watch: {
speedDialLoadingState(state) {
if (state === 'requesting') {
startLoading();
}
else if (state === 'failed') {
stopLoading();
showGlobalError(this.speedDialLoadingError);
}
else if (state === 'succeeded') {
stopLoading();
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -46,7 +46,12 @@
"privacy": "Privacy"
},
"reminder": {
"title": "Reminder"
"title": "Reminder",
"subTitle": "Set your personal alarm"
},
"speeddial": {
"title": "Speed Dial",
"subTitle": "Set your speed dials"
},
"pbxConfiguration": {
"title": "PBX Configuration",
@ -127,7 +132,13 @@
"tabLabelAll": "All",
"tabLabelCalls": "Calls",
"tabLabelFaxes": "Faxes",
"tabLabelVoicemails": "Voicemails"
"tabLabelVoicemails": "Voicemails",
"fax": "Fax",
"page": "page",
"pages": "pages",
"voicemail": "Voicemail",
"duration": "Duration",
"seconds": "seconds"
},
"reminder": {
"toggleEnabled": "Reminder is enabled",
@ -239,6 +250,7 @@
"callForward": "Call Forward",
"callBlocking": "Call Blocking",
"reminder": "Reminder",
"speeddial": "Speed Dial",
"buddyList": "Buddy List",
"welcome": "Welcome"
},
@ -351,5 +363,11 @@
"cancel": "Cancel",
"createFaxErrorMessage": "An error occured while trying to send the fax. Please try again.",
"createFaxSuccessMessage": "Sending fax completed successfully."
},
"speedDial": {
"whenIDial": "When I dial {slot} ...",
"ring": "ring",
"loadSpeedDialErrorMessage": "An error occured while trying to load the speed dials. Please try again.",
"noResultsMessage": "No speed dials found"
}
}

@ -10,6 +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 PbxConfigurationGroups from './components/pages/PbxConfiguration/CscPbxGroups'
import PbxConfigurationSeats from './components/pages/PbxConfiguration/CscPbxSeats'
import PbxConfigurationDevices from './components/pages/PbxConfiguration/CscPbxDevices'
@ -92,6 +93,13 @@ export default [
title: i18n.t('navigation.reminder.title')
}
},
{
path: 'speeddial',
component: SpeedDial,
meta: {
title: i18n.t('navigation.speeddial.title')
}
},
{
path: 'pbx-configuration/groups',
component: PbxConfigurationGroups,

@ -2,6 +2,7 @@
'use strict';
import _ from 'lodash';
import { RequestState } from './common'
import { i18n } from '../i18n';
import {
getSourcesets,
@ -26,13 +27,6 @@ import {
deleteSourceFromSourcesetByIndex
} from '../api/call-forward';
const RequestState = {
initial: 'initial',
requesting: 'requesting',
succeeded: 'succeeded',
failed: 'failed'
};
export default {
namespaced: true,
state: {
@ -95,7 +89,7 @@ export default {
hasFaxCapability(state, getters, rootState, rootGetters) {
return rootGetters['user/hasFaxCapability'];
},
getSubscriberId(state, getters, rootState, rootGetters) {
subscriberId(state, getters, rootState, rootGetters) {
return rootGetters['user/getSubscriberId'];
},
getForm(state) {
@ -509,7 +503,7 @@ export default {
form.destination = options.form.destination;
}
updatedOptions = {
subscriberId: context.getters.getSubscriberId,
subscriberId: context.getters.subscriberId,
data: form,
groupName: context.getters.getGroupName,
id: context.getters.getDestinationsetId,
@ -585,7 +579,7 @@ export default {
changePositionOfDestination({
destinations: clonedDestinations,
id: options.id,
subscriberId: context.getters.getSubscriberId
subscriberId: context.getters.subscriberId
}).then(() => {
context.commit('changeDestinationSucceeded');
}).catch((err) => {
@ -597,7 +591,7 @@ export default {
loadTimesetTimes(context, options) {
loadTimesetTimes({
timeset: options.timeset,
subscriberId: context.getters.getSubscriberId
subscriberId: context.getters.subscriberId
}).then((result) => {
context.commit('loadTimesSucceeded', result);
});
@ -613,7 +607,7 @@ export default {
delete time.to;
});
deleteTimeFromTimeset({
subscriberId: context.getters.getSubscriberId,
subscriberId: context.getters.subscriberId,
timesetId: context.getters.getTimesetId,
times: clonedTimes
}).then(() => {
@ -636,7 +630,7 @@ export default {
resetTimesetByName(context, name) {
context.commit('resetTimeRequesting');
resetTimesetByName({
id: context.getters.getSubscriberId,
id: context.getters.subscriberId,
name: name
}).then(() => {
context.commit('resetTimesetState');
@ -651,7 +645,7 @@ export default {
time: options.time,
weekday: options.weekday,
name: options.name,
subscriberId: context.getters.getSubscriberId
subscriberId: context.getters.subscriberId
}).then(() => {
context.commit('addTimeSucceeded');
}).catch((err) => {
@ -674,7 +668,7 @@ export default {
context.commit('loadDestinationRequesting');
loadDestinations({
timeset: options.timeset,
subscriberId: context.getters.getSubscriberId
subscriberId: context.getters.subscriberId
}).then((result) => {
context.commit('loadDestinations', result);
context.commit('loadDestinationSucceeded');
@ -687,7 +681,7 @@ export default {
createSourcesetWithSource({
sourcesetName: options.sourcesetName,
source: options.source,
subscriberId: context.getters.getSubscriberId,
subscriberId: context.getters.subscriberId,
mode: options.mode
}).then(() => {
context.commit('setLastAddedSourceset', options.sourcesetName);

@ -10,6 +10,7 @@ import ConversationsModule from './conversations'
import LayoutModule from './layout'
import PbxConfigModule from './pbx-config/index'
import ReminderModule from './reminder'
import SpeedDialModule from './speed-dial'
import UserModule from './user'
import CommunicationModule from './communication'
@ -24,6 +25,7 @@ export const store = new Vuex.Store({
layout: LayoutModule,
pbxConfig: PbxConfigModule,
reminder: ReminderModule,
speedDial: SpeedDialModule,
user: UserModule,
communication: CommunicationModule
},

@ -21,6 +21,9 @@ export default {
reminderError: null
},
getters: {
subscriberId(state, getters, rootState, rootGetters) {
return rootGetters['user/getSubscriberId'];
},
isReminderActive(state) {
return state.reminder !== null && state.reminder.active === true;
},
@ -80,7 +83,7 @@ export default {
loadReminder(context) {
return new Promise((resolve, reject)=>{
context.commit('reminderLoading');
getReminder(localStorage.getItem('subscriberId')).then((reminder)=>{
getReminder(context.getters.subscriberId).then((reminder)=>{
context.commit('reminderLoaded', reminder);
resolve();
}).catch((err)=>{

@ -0,0 +1,62 @@
'use strict';
import { i18n } from '../i18n';
import { RequestState } from './common'
import {
getSpeedDials
} from '../api/speed-dial';
export default {
namespaced: true,
state: {
assignedSlots: [],
slotOptions: [],
speedDialLoadingState: RequestState.initiated,
speedDialError: null
},
getters: {
reminderLoadingState(state) {
return state.reminderLoadingState;
},
reminderError(state) {
return state.reminderError;
},
subscriberId(state, getters, rootState, rootGetters) {
return rootGetters['user/getSubscriberId'];
},
assignedSlots(state) {
return state.assignedSlots;
},
speedDialLoadingState(state) {
return state.speedDialLoadingState;
},
speedDialLoadingError(state) {
return state.speedDialLoadingError || i18n.t('speedDial.loadSpeedDialErrorMessage');
}
},
mutations: {
speedDialRequesting(state) {
state.speedDialLoadingState = RequestState.requesting;
state.speedDialLoadingError = null;
},
speedDialSucceeded(state, slots) {
state.speedDialLoadingState = RequestState.succeeded;
state.assignedSlots = slots;
state.speedDialLoadingError = null;
},
speedDialFailed(state, error) {
state.speedDialLoadingState = RequestState.failed;
state.speedDialLoadingError = error;
}
},
actions: {
loadSpeedDials(context) {
context.commit('speedDialRequesting');
getSpeedDials(context.getters.subscriberId).then((slots) => {
context.commit('speedDialSucceeded', slots);
}).catch((error) => {
context.commit('speedDialFailed', error);
});
}
}
};

@ -0,0 +1,99 @@
'use strict';
import Vue from 'vue';
import VueResource from 'vue-resource';
import {
getFieldList
} from '../../src/api/common';
import {
getSpeedDials
} from '../../src/api/speed-dial';
import { assert } from 'chai';
Vue.use(VueResource);
describe('Speed Dials', function(){
const subscriberId = 123;
it('should get list of subscriber specific speed dials', 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@10.15.17.240",
"slot" : "*9"
},
{
"destination" : "sip:22222222@10.15.17.240",
"slot" : "*0"
},
{
"destination" : "sip:43665522@10.15.17.240",
"slot" : "*3"
}
]
};
let fieldList = [
{
"destination" : "sip:22222222@10.15.17.240",
"slot" : "*0"
},
{
"destination" : "sip:43665522@10.15.17.240",
"slot" : "*3"
},
{
"destination" : "sip:439965050@10.15.17.240",
"slot" : "*9"
}
];
Vue.http.interceptors = [];
Vue.http.interceptors.unshift((request, next)=>{
next(request.respondWith(JSON.stringify(data), {
status: 200
}));
});
getSpeedDials(subscriberId).then((result)=>{
assert.deepEqual(result, fieldList);
done();
}).catch((err)=>{
done(err);
});
});
});
Loading…
Cancel
Save