TT#40083 User wants to configure voicebox settings

What has been done:
- TT#40083, Voicebox: As a Customer, I want to configure voicebox
  settings
- TT#44658, Voicebox: As a Customer, I want to change the E-Mail-Address
- TT#44657, Voicebox: As a Customer, I want to change the PIN
- TT#40083, Voicebox: As a Customer, I want to enable/disable deletion
  of voicemail after mail delivery

Change-Id: Ic11973de74e3a3e3d2da320a267182a051fa1571
changes/74/23774/7
raxelsen 7 years ago
parent 5f1133a150
commit 74dc8cfe65

@ -0,0 +1,52 @@
import _ from 'lodash'
import {
get,
patchReplace
} from './common'
export function getVoiceboxSettings(subscriberId) {
return new Promise((resolve, reject) => {
get({
path: `api/voicemailsettings/${subscriberId}`
}).then((result)=>{
let settings = _.clone(result);
delete settings._links;
resolve(settings);
}).catch((err)=>{
reject(err);
});
});
}
export function setVoiceboxDelete(options) {
return patchReplace({
path: `api/voicemailsettings/${options.subscriberId}`,
fieldPath: 'delete',
value: options.value
});
}
export function setVoiceboxAttach(options) {
return patchReplace({
path: `api/voicemailsettings/${options.subscriberId}`,
fieldPath: 'attach',
value: options.value
});
}
export function setVoiceboxPin(options) {
return patchReplace({
path: `api/voicemailsettings/${options.subscriberId}`,
fieldPath: 'pin',
value: options.value
});
}
export function setVoiceboxEmail(options) {
return patchReplace({
path: `api/voicemailsettings/${options.subscriberId}`,
fieldPath: 'email',
value: options.value
});
}

@ -232,6 +232,16 @@
<q-item-main :label="$t('navigation.pbxConfiguration.devices')" />
</q-side-link>
</q-collapsible>
<q-side-link
item
to="/user/voicebox"
>
<q-item-side icon="voicemail"/>
<q-item-main
:label="$t('navigation.voicebox.title')"
:sublabel="$t('navigation.voicebox.subTitle')"
/>
</q-side-link>
</q-list>
<router-view />
<csc-call

@ -30,7 +30,7 @@
</q-field>
<div v-if="addFormEnabled">
<q-field :error="addFormError" :error-label="$t('pages.callBlocking' + suffix + '.addInputError')">
<q-input type="text" float-label="Number" v-model="newNumber" clearable @keyup.enter="addNumber()" />
<q-input type="text" :float-label="$t('callBlocking.number')" v-model="newNumber" clearable @keyup.enter="addNumber()" />
</q-field>
<q-btn flat @click="disableAddForm()">{{ $t('buttons.cancel') }}</q-btn>
<q-btn flat color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn>
@ -43,7 +43,7 @@
<q-icon v-if="!(editing && editingIndex == index) && enabled == 'whitelist'" name="check" color="primary" size="22px"/>
<span class="blocked-number-title" v-if="!(editing && editingIndex == index)"
@click="editNumber(index)">{{ number }}</span>
<q-input autofocus v-if="editing && editingIndex == index" type="text" float-label="Number"
<q-input autofocus v-if="editing && editingIndex == index" type="text" :float-label="$t('callBlocking.number')"
v-model="editingNumber" @keyup.enter="saveNumber(index)" />
<q-btn color="primary" flat v-if="editing && editingIndex == index" slot="right"
icon="fa-save" @click="saveNumber(index)" class="cursor-pointer"><span class="gt-sm">{{ $t('buttons.save') }}</span></q-btn>

@ -154,7 +154,7 @@
directives: {
BackToTop
},
mounted() {
created() {
this.$store.commit('conversations/resetList');
},
inject: ['layout'],

@ -28,9 +28,9 @@
<q-item-tile
sublabel
>
{{ t$('pages.conversations.duration') }}
{{ $t('pages.conversations.duration') }}
{{ voiceMail.duration }}
{{ t$('pages.conversations.seconds') }}
{{ $t('pages.conversations.seconds') }}
</q-item-tile>
<q-item-tile>
<csc-voice-mail-player

@ -3,7 +3,7 @@
class="csc-simple-page"
>
<q-field
class="reminder-field"
class="csc-form-field"
>
<q-toggle
:disable="isReminderLoading"
@ -15,7 +15,7 @@
/>
</q-field>
<q-field
class="reminder-field"
class="csc-form-field"
>
<q-datetime
format24h
@ -28,7 +28,7 @@
/>
</q-field>
<q-field
class="reminder-field"
class="csc-form-field"
>
<q-option-group
color="positive"
@ -164,7 +164,4 @@
</script>
<style lang="stylus" rel="stylesheet/stylus">
.reminder-field {
margin-bottom: 40px;
}
</style>

@ -0,0 +1,202 @@
<template>
<div>
<q-field
class="csc-form-field"
icon="lock"
:error-label="pinErrorMessage"
>
<q-input
:loading="pinRequesting"
:disable="pinRequesting"
:float-label="$t('voicebox.label.changePin')"
v-model="changes.pin"
:after="pinButtons"
@keyup.enter="updatePin"
@input="$v.changes.pin.$touch"
@blur="$v.changes.pin.$touch"
:error="$v.changes.pin.$error"
/>
</q-field>
<q-field
class="csc-form-field"
icon="email"
:error-label="emailErrorMessage"
>
<q-input
:loading="emailRequesting"
:disable="emailRequesting"
:float-label="$t('voicebox.label.changeEmail')"
v-model="changes.email"
:after="emailButtons"
@keyup.enter="updateEmail"
@input="$v.changes.email.$touch"
@blur="$v.changes.email.$touch"
:error="$v.changes.email.$error"
/>
</q-field>
<q-field class="csc-form-field">
<q-toggle
:disable="deleteRequesting || !canToggleDelete"
:label="deleteLabel"
v-model="changes.delete"
@input="toggle('delete')"
checked-icon="delete"
unchecked-icon="delete"
/>
</q-field>
<q-field class="csc-form-field">
<q-toggle
:disable="attachRequesting || !canToggleAttachment"
:label="attachLabel"
v-model="changes.attach"
@input="toggle('attach')"
checked-icon="attach_file"
unchecked-icon="attach_file"
/>
</q-field>
</div>
</template>
<script>
import {
maxLength,
email
} from 'vuelidate/lib/validators'
import {
QField,
QInput,
QToggle
} from 'quasar-framework'
export default {
name: 'csc-voicebox-settings',
props: [
'settings',
'deleteRequesting',
'attachRequesting',
'pinRequesting',
'emailRequesting',
'attachLabel',
'deleteLabel'
],
data () {
return {
changes: this.getSettings()
}
},
components: {
QField,
QInput,
QToggle
},
validations: {
changes: {
pin: {
maxLength: maxLength(64)
},
email: {
email
}
}
},
computed: {
pinErrorMessage() {
return this.$t('validationErrors.maxLength', {
field: this.$t('voicebox.pin'),
maxLength: this.$v.changes.pin.$params.maxLength.max
});
},
emailErrorMessage() {
return this.$t('validationErrors.email');
},
canToggleDelete() {
return this.settings.attach;
},
canToggleAttachment() {
return !this.settings.delete;
},
pinHasChanged() {
return this.changes.pin !== this.settings.pin;
},
emailHasChanged() {
return this.changes.email !== this.settings.email;
},
pinButtons() {
let buttons = [];
let self = this;
if (this.pinHasChanged) {
buttons.push({
icon: 'check',
error: false,
handler (event) {
event.stopPropagation();
self.updatePin();
}
}, {
icon: 'clear',
error: false,
handler (event) {
event.stopPropagation();
self.resetFields();
}
}
);
}
return buttons;
},
emailButtons() {
let buttons = [];
let self = this;
if (this.emailHasChanged) {
buttons.push({
icon: 'check',
error: false,
handler (event) {
event.stopPropagation();
self.updateEmail();
}
}, {
icon: 'clear',
error: false,
handler (event) {
event.stopPropagation();
self.resetFields();
}
}
);
}
return buttons;
}
},
methods: {
getSettings() {
return {
delete: this.settings.delete,
attach: this.settings.attach,
pin: this.settings.pin,
email: this.settings.email
}
},
resetFields() {
this.changes = this.getSettings();
},
toggle(field) {
if (field === 'delete') {
this.$store.dispatch('voicebox/toggleDelete');
}
else if (field === 'attach') {
this.$store.dispatch('voicebox/toggleAttach');
}
},
updatePin() {
this.$store.dispatch('voicebox/updatePin', this.changes.pin);
},
updateEmail() {
this.$store.dispatch('voicebox/updateEmail', this.changes.email);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -0,0 +1,121 @@
<template>
<csc-page class="csc-simple-page">
<csc-voicebox-settings
v-if="isSettingsLoaded"
:settings="voiceboxSettings"
:deleteRequesting="isDeleteRequesting"
:attachRequesting="isAttachRequesting"
:pinRequesting="isPinRequesting"
:emailRequesting="isEmailRequesting"
:deleteLabel="deleteLabel"
:attachLabel="attachLabel"
/>
</csc-page>
</template>
<script>
import { mapGetters } from 'vuex'
import CscPage from '../../CscPage'
import CscVoiceboxSettings from './CscVoiceboxSettings'
import {
startLoading,
stopLoading,
showToast,
showGlobalError
} from '../../../helpers/ui'
export default {
data () {
return {
}
},
components: {
CscPage,
CscVoiceboxSettings
},
created() {
this.$store.dispatch('voicebox/getVoiceboxSettings');
},
computed: {
...mapGetters('voicebox', [
'voiceboxSettings',
'deleteLabel',
'attachLabel',
'isDeleteRequesting',
'isAttachRequesting',
'isPinRequesting',
'isEmailRequesting',
'isSettingsLoaded',
'loadingState',
'loadingError',
'toggleDeleteState',
'toggleDeleteError',
'toggleAttachState',
'toggleAttachError',
'updatePinState',
'updatePinError',
'updateEmailState',
'updateEmailError',
])
},
watch: {
loadingState(state) {
if (state === 'requesting') {
startLoading();
}
else if (state === 'succeeded') {
stopLoading();
}
else if (state === 'failed') {
stopLoading();
showGlobalError(this.loadingError);
}
},
toggleDeleteState(state) {
if (state === 'requesting') {
startLoading();
}
else if (state === 'succeeded') {
stopLoading();
showToast(this.$t('voicebox.toggleDeleteSuccessMessage'));
}
else if (state === 'failed') {
stopLoading();
showGlobalError(this.toggleDeleteError);
}
},
toggleAttachState(state) {
if (state === 'requesting') {
startLoading();
}
else if (state === 'succeeded') {
stopLoading();
showToast(this.$t('voicebox.toggleAttachSuccessMessage'));
}
else if (state === 'failed') {
stopLoading();
showGlobalError(this.toggleAttachError);
}
},
updatePinState(state) {
if (state === 'succeeded') {
showToast(this.$t('voicebox.updatePinSuccessMessage'));
}
else if (state === 'failed') {
showGlobalError(this.updatePinError);
}
},
updateEmailState(state) {
if (state === 'succeeded') {
showToast(this.$t('voicebox.updateEmailSuccessMessage'));
}
else if (state === 'failed') {
showGlobalError(this.updateEmailError);
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -33,7 +33,8 @@
"alphaNum": "{field} must consist of numeric characters only",
"inputNumber": "Input a phone number",
"inputValidNumber": "Input a valid phone number",
"fieldRequiredXor": "{fieldOne} or {fieldTwo} is required"
"fieldRequiredXor": "{fieldOne} or {fieldTwo} is required",
"email": "Input a valid email address"
},
"navigation": {
"home": {
@ -72,6 +73,10 @@
"groups": "Groups",
"seats": "Seats",
"devices": "Devices"
},
"voicebox": {
"title": "Voicebox",
"subTitle": "Set your voicebox settings"
}
},
"pages": {
@ -382,7 +387,8 @@
"privacyEnabledToast": "Your number is hidden to the callee",
"privacyEnabledLabel": "Your number is hidden to the callee",
"privacyDisabledToast": "Your number is visible to the callee",
"privacyDisabledLabel": "Your number is visible to the callee"
"privacyDisabledLabel": "Your number is visible to the callee",
"number": "Number"
},
"communication": {
"sendFax": "Send Fax",
@ -420,5 +426,25 @@
"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}"
},
"voicebox": {
"label": {
"changeEmail": "Change Email",
"changePin": "Change PIN",
"deletionEnabled": "Voicemail will be deleted after email notification is delivered",
"deletionDisabled": "Voicemail will not be deleted after email notification is delivered",
"attachmentEnabled": "Voicemail will be attached to email notification",
"attachmentDisabled": "Voicemail will not be attached to email notification"
},
"pin": "PIN",
"loadSettingsErrorMessage": "An error occured while trying to load the settings. Please reload the page or check your network connection.",
"toggleDeleteSuccessMessage": "Toggled deletion successfully.",
"toggleDeleteErrorMessage": "An error occured while trying to toggle the delete option. Please try again.",
"toggleAttachSuccessMessage": "Toggled attachment successfully.",
"toggleAttachErrorMessage": "An error occured while trying to toggle the attach option. Please try again.",
"updatePinSuccessMessage": "Changed PIN successfully.",
"updatePinErrorMessage": "An error occured while trying to update the pin field. Please try again.",
"updateEmailSuccessMessage": "Changed email successfully.",
"updateEmailErrorMessage": "An error occured while trying to update the email field. Please try again."
}
}

@ -14,6 +14,7 @@ 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'
import Voicebox from './components/pages/Voicebox/Voicebox';
import Login from './components/Login'
import Error404 from './components/Error404'
@ -90,14 +91,16 @@ export default [
path: 'reminder',
component: Reminder,
meta: {
title: i18n.t('navigation.reminder.title')
title: i18n.t('navigation.reminder.title'),
subtitle: i18n.t('navigation.reminder.subTitle')
}
},
{
path: 'speeddial',
component: SpeedDial,
meta: {
title: i18n.t('navigation.speeddial.title')
title: i18n.t('navigation.speeddial.title'),
subtitle: i18n.t('navigation.speeddial.subTitle')
}
},
{
@ -123,6 +126,14 @@ export default [
title: i18n.t('navigation.pbxConfiguration.title'),
subtitle: i18n.t('navigation.pbxConfiguration.devices')
}
},
{
path: 'voicebox',
component: Voicebox,
meta: {
title: i18n.t('navigation.voicebox.title'),
subtitle: i18n.t('navigation.voicebox.subTitle')
}
}
]
},

@ -13,6 +13,7 @@ import ReminderModule from './reminder'
import SpeedDialModule from './speed-dial'
import UserModule from './user'
import CommunicationModule from './communication'
import VoiceboxModule from './voicebox'
Vue.use(Vuex);
@ -27,7 +28,8 @@ export const store = new Vuex.Store({
reminder: ReminderModule,
speedDial: SpeedDialModule,
user: UserModule,
communication: CommunicationModule
communication: CommunicationModule,
voicebox: VoiceboxModule
},
getters: {
pageTitle(state) {

@ -10,7 +10,6 @@ import {
setReminderRecurrence
} from '../api/reminder';
export default {
namespaced: true,
state: {

@ -0,0 +1,234 @@
'use strict';
import _ from 'lodash'
import { RequestState } from './common'
import {
getVoiceboxSettings,
setVoiceboxDelete,
setVoiceboxAttach,
setVoiceboxPin,
setVoiceboxEmail
} from '../api/voicebox';
import { i18n } from '../i18n';
export default {
namespaced: true,
state: {
voiceboxSettings: {
attach: null,
delete: null,
email: '',
id: null,
pin: null,
sms_number: ''
},
loadingState: RequestState.initial,
loadingError: null,
toggleDeleteState: RequestState.initial,
toggleDeleteError: null,
toggleAttachState: RequestState.initial,
toggleAttachError: null,
updatePinState: RequestState.initial,
updatePinError: null,
updateEmailState: RequestState.initial,
updateEmailError: null
},
getters: {
subscriberId(state, getters, rootState, rootGetters) {
return parseInt(rootGetters['user/getSubscriberId']);
},
isSettingsLoaded(state) {
return state.loadingState === 'succeeded';
},
isDeleteRequesting(state) {
return state.toggleDeleteState === 'requesting';
},
isAttachRequesting(state) {
return state.toggleAttachState === 'requesting';
},
isPinRequesting(state) {
return state.updatePinState === 'requesting';
},
isEmailRequesting(state) {
return state.updateEmailState === 'requesting';
},
loadingState(state) {
return state.loadingState;
},
loadingError(state) {
return state.loadingError ||
i18n.t('voicebox.loadSettingsErrorMessage');
},
voiceboxDelete(state) {
return _.get(state.voiceboxSettings, 'delete', false);
},
voiceboxAttach(state) {
return _.get(state.voiceboxSettings, 'attach', false);
},
deleteLabel(state) {
return state.voiceboxSettings.delete ?
i18n.t('voicebox.label.deletionEnabled') :
i18n.t('voicebox.label.deletionDisabled');
},
attachLabel(state) {
return state.voiceboxSettings.attach ?
i18n.t('voicebox.label.attachmentEnabled') :
i18n.t('voicebox.label.attachmentDisabled');
},
voiceboxSettings(state) {
return state.voiceboxSettings;
},
toggleDeleteState(state) {
return state.toggleDeleteState;
},
toggleDeleteError(state) {
return state.toggleDeleteError ||
i18n.t('voicebox.toggleDeleteErrorMessage');
},
toggleAttachState(state) {
return state.toggleAttachState;
},
toggleAttachError(state) {
return state.toggleAttachError ||
i18n.t('voicebox.toggleAttachErrorMessage');
},
updatePinState(state) {
return state.updatePinState;
},
updatePinError(state) {
return state.updatePinError ||
i18n.t('voicebox.updatePinErrorMessage');
},
updateEmailState(state) {
return state.updateEmailState;
},
updateEmailError(state) {
return state.updateEmailError ||
i18n.t('voicebox.updateEmailErrorMessage');
}
},
mutations: {
loadingRequesting(state) {
state.loadingState = RequestState.requesting;
state.loadingError = null;
},
loadingSucceeded(state, settings) {
state.loadingState = RequestState.succeeded;
state.voiceboxSettings = settings;
state.loadingError = null;
},
loadingFailed(state, error) {
state.loadingState = RequestState.failed;
state.loadingError = error;
},
toggleDeleteRequesting(state) {
state.toggleDeleteState = RequestState.requesting;
state.toggleDeleteError = null;
},
toggleDeleteSucceeded(state) {
state.toggleDeleteState = RequestState.succeeded;
state.toggleDeleteError = null;
},
toggleDeleteFailed(state, error) {
state.toggleDeleteState = RequestState.failed;
state.toggleDeleteError = error;
},
toggleAttachRequesting(state) {
state.toggleAttachState = RequestState.requesting;
state.toggleAttachError = null;
},
toggleAttachSucceeded(state) {
state.toggleAttachState = RequestState.succeeded;
state.toggleAttachError = null;
},
toggleAttachFailed(state, error) {
state.toggleAttachState = RequestState.failed;
state.toggleAttachError = error;
},
updatePinRequesting(state) {
state.updatePinState = RequestState.requesting;
state.updatePinError = null;
},
updatePinSucceeded(state) {
state.updatePinState = RequestState.succeeded;
state.updatePinError = null;
},
updatePinFailed(state, error) {
state.updatePinState = RequestState.failed;
state.updatePinError = error;
},
updateEmailRequesting(state) {
state.updateEmailState = RequestState.requesting;
state.updateEmailError = null;
},
updateEmailSucceeded(state) {
state.updateEmailState = RequestState.succeeded;
state.updateEmailError = null;
},
updateEmailFailed(state, error) {
state.updateEmailState = RequestState.failed;
state.updateEmailError = error;
}
},
actions: {
getVoiceboxSettings(context) {
context.commit('loadingRequesting');
getVoiceboxSettings(context.getters.subscriberId).then((settings) => {
context.commit('loadingSucceeded', settings);
}).catch((err) => {
context.commit('loadingFailed', err.message);
})
},
toggleDelete(context) {
context.commit('toggleDeleteRequesting');
setVoiceboxDelete({
subscriberId: context.getters.subscriberId,
value: !context.getters.voiceboxDelete
}).then(() => {
context.commit('toggleDeleteSucceeded');
context.dispatch('getVoiceboxSettings');
}).catch((err) => {
context.commit('toggleDeleteFailed', err.message);
context.dispatch('getVoiceboxSettings');
});
},
toggleAttach(context) {
context.commit('toggleAttachRequesting');
setVoiceboxAttach({
subscriberId: context.getters.subscriberId,
value: !context.getters.voiceboxAttach
}).then(() => {
context.commit('toggleAttachSucceeded');
context.dispatch('getVoiceboxSettings');
}).catch((err) => {
context.commit('toggleAttachFailed', err.message);
context.dispatch('getVoiceboxSettings');
});
},
updatePin(context, value) {
context.commit('updatePinRequesting');
setVoiceboxPin({
subscriberId: context.getters.subscriberId,
value: value
}).then(() => {
context.commit('updatePinSucceeded');
context.dispatch('getVoiceboxSettings');
}).catch((err) => {
context.commit('updatePinFailed', err.message);
});
},
updateEmail(context, value) {
context.commit('updateEmailRequesting');
setVoiceboxEmail({
subscriberId: context.getters.subscriberId,
value: value
}).then(() => {
context.commit('updateEmailSucceeded');
context.dispatch('getVoiceboxSettings');
}).catch((err) => {
context.commit('updateEmailFailed', err.message);
});
}
}
};

@ -112,3 +112,8 @@
padding-left 8px
padding-right 8px
.csc-form-field
margin-bottom 40px
.q-field-icon
color $primary

@ -0,0 +1,80 @@
'use strict';
import Vue from 'vue';
import VueResource from 'vue-resource';
import {
get
} from '../../src/api/common';
import {
getVoiceboxSettings
} from '../../src/api/voicebox';
import { assert } from 'chai';
Vue.use(VueResource);
describe('Voicebox', function(){
const subscriberId = 123;
it('should get subscriber\'s voicebox settings', function(done){
let data = {
"_links" : {
"collection" : {
"href" : "/api/voicemailsettings/"
},
"curies" : {
"href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
"name" : "ngcp",
"templated" : true
},
"ngcp:journal" : [
{
"href" : "/api/voicemailsettings/123/journal/"
}
],
"ngcp:subscribers" : [
{
"href" : "/api/subscribers/123"
}
],
"profile" : {
"href" : "http://purl.org/sipwise/ngcp-api/"
},
"self" : {
"href" : "/api/voicemailsettings/123"
}
},
"attach" : true,
"delete" : false,
"email" : "",
"id" : 123,
"pin" : "1234",
"sms_number" : ""
};
let settings = {
"attach" : true,
"delete" : false,
"email" : "",
"id" : 123,
"pin" : "1234",
"sms_number" : ""
};
Vue.http.interceptors = [];
Vue.http.interceptors.unshift((request, next)=>{
next(request.respondWith(JSON.stringify(data), {
status: 200
}));
});
getVoiceboxSettings(subscriberId).then((result)=>{
assert.deepEqual(result, settings);
done();
}).catch((err)=>{
done(err);
});
});
});

@ -0,0 +1,32 @@
'use strict';
import VoiceboxModule from '../../src/store/voicebox';
import { assert } from 'chai';
describe('Voicebox', function(){
it('should load all voicebox settings into store', function(){
let state = {
voiceboxSettings: {
attach: null,
delete: null,
email: '',
id: null,
pin: null,
sms_number: ''
}
};
let settings = {
attach: true,
delete: false,
email: '',
id: 123,
pin: 1234,
sms_number: ''
};
VoiceboxModule.mutations.loadingSucceeded(state, settings);
assert.deepEqual(state.voiceboxSettings, settings);
});
});
Loading…
Cancel
Save