diff --git a/src/api/common.js b/src/api/common.js index 498fa822..00b4f256 100644 --- a/src/api/common.js +++ b/src/api/common.js @@ -8,7 +8,7 @@ export const LIST_DEFAULT_ROWS = 25; export const LIST_ALL_ROWS = 1000; export function getList(options) { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { options = options || {}; options = _.merge({ all: false, @@ -20,7 +20,7 @@ export function getList(options) { 'Accept': 'application/json' } }, options); - Promise.resolve().then(()=>{ + Promise.resolve().then(() => { if(options.all === true) { options.params.rows = LIST_ALL_ROWS; } @@ -28,7 +28,7 @@ export function getList(options) { params: options.params, headers: options.headers }); - }).then((res)=>{ + }).then((res) => { let body = getJsonBody(res.body); if(options.all === true && body.total_count > LIST_ALL_ROWS) { return Vue.http.get(options.path, { @@ -41,7 +41,7 @@ export function getList(options) { else { return Promise.resolve(res); } - }).then((res)=>{ + }).then((res) => { let body = getJsonBody(res.body); let lastPage = Math.ceil( body.total_count / options.params.rows ); if(options.all === true) { @@ -51,21 +51,21 @@ export function getList(options) { items: _.get(body, options.root, []), lastPage: lastPage }); - }).catch((err)=>{ + }).catch((err) => { reject(err); }); }); } export function get(options) { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { return Vue.http.get(options.path, { headers: { 'Accept': 'application/json' } - }).then((result)=>{ + }).then((result) => { resolve(getJsonBody(result.body)); - }).catch((err)=>{ + }).catch((err) => { if(err.status && err.status >= 400) { reject(new Error(err.body.message)); } @@ -77,7 +77,7 @@ export function get(options) { } export function patchReplace(options) { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { Vue.http.patch(options.path, [{ op: 'replace', path: '/'+ options.fieldPath, @@ -87,9 +87,9 @@ export function patchReplace(options) { 'Content-Type': 'application/json-patch+json', 'Prefer': 'return=minimal' } - }).then((result)=>{ + }).then((result) => { resolve(result); - }).catch((err)=>{ + }).catch((err) => { if(err.status >= 400) { reject(new Error(err.body.message)); } @@ -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); + }); + }); +} diff --git a/src/api/speed-dial.js b/src/api/speed-dial.js new file mode 100644 index 00000000..50796e97 --- /dev/null +++ b/src/api/speed-dial.js @@ -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); + }); + }); +} diff --git a/src/components/layouts/Default.vue b/src/components/layouts/Default.vue index 2654fe60..5903d6d7 100644 --- a/src/components/layouts/Default.vue +++ b/src/components/layouts/Default.vue @@ -191,8 +191,15 @@ > + + + + - + - + {{ typeTerm }} {{ direction }} {{ number | destinationFormat }} - {{ call.start_time | smartTime }} + + {{ call.start_time | smartTime }} - Fax - {{ direction }} - {{ fax.caller | numberFormat }} + + {{ $t('pages.conversations.fax') }} + + + {{ direction }} + + + {{ fax.caller | numberFormat }} + {{ fax.pages }} page + > + {{ fax.pages }} {{ $t('pages.conversations.page') }} {{ fax.pages }} pages + > + {{ fax.pages }} {{ $t('pages.conversations.pages') }} - Voicemail - {{ direction }} - {{ voiceMail.caller | numberFormat }} + + {{ $t('pages.conversations.voicemail') }} + + + {{ direction }} + + + {{ voiceMail.caller | numberFormat }} + - Duration: {{ voiceMail.duration }} seconds + {{ t$('pages.conversations.duration') }} + {{ voiceMail.duration }} + {{ t$('pages.conversations.seconds') }} + + + + + + + + {{ $t('pages.home.cards.speeddial') }} + + + + diff --git a/src/components/pages/SpeedDial.vue b/src/components/pages/SpeedDial.vue new file mode 100644 index 00000000..8a06201c --- /dev/null +++ b/src/components/pages/SpeedDial.vue @@ -0,0 +1,116 @@ + + + + + + + + + {{ $t('speedDial.whenIDial', { slot: assigned.slot }) }} + + + + {{ $t('speedDial.ring') }} + {{ assigned.destination | destinationFormat }} + + + + + + + + + + + {{ $t('speedDial.noResultsMessage') }} + + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 334e83eb..b5bcb47b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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" } } diff --git a/src/routes.js b/src/routes.js index 368d7025..5e830b8e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -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, diff --git a/src/store/call-forward.js b/src/store/call-forward.js index 19831a37..95923a4d 100644 --- a/src/store/call-forward.js +++ b/src/store/call-forward.js @@ -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); diff --git a/src/store/index.js b/src/store/index.js index 121681ff..a6810c5f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 }, diff --git a/src/store/reminder.js b/src/store/reminder.js index fb91a77b..7e6917a8 100644 --- a/src/store/reminder.js +++ b/src/store/reminder.js @@ -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)=>{ diff --git a/src/store/speed-dial.js b/src/store/speed-dial.js new file mode 100644 index 00000000..a061c59a --- /dev/null +++ b/src/store/speed-dial.js @@ -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); + }); + } + } +}; diff --git a/t/api/speed-dial.js b/t/api/speed-dial.js new file mode 100644 index 00000000..b2843e37 --- /dev/null +++ b/t/api/speed-dial.js @@ -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); + }); + }); + +});