TT#75404 CF: As a Customer, I want to add phone numbers in order to forward calls to these numbers

What is missing:
- loader during requests
- message on destination saved
- timeout requests on filed blur (current on value change => multiple requests)
- the form to add destinations currently shows up a the bottom of the destinationset instead as popover
- (cosmetic) + icon instead of char before ADD FORWARD label

Change-Id: I61986bcdc9f157787c1cd9d690c752b9fd44fe24
changes/96/37996/6
Carlo Venusino 6 years ago committed by Hans-Peter Herzog
parent ad8cda290d
commit 8a1adda615

@ -319,6 +319,17 @@ export function addNewDestinationset() {
});
}
export function addNewDestinationsetWithName(destinationsetName) {
return new Promise((resolve, reject) => {
Vue.http.post('api/cfdestinationsets/', { name: destinationsetName })
.then((response) => {
resolve(_.last(_.split(response.headers.get('Location'), '/')));
}).catch((err) => {
reject(err);
});
});
}
export function addDestinationToExistingGroup(options) {
return new Promise((resolve, reject)=> {
Promise.resolve().then(() => {

@ -0,0 +1,165 @@
<template>
<csc-page
class="csc-simple-page"
>
<div
class="row"
>
<div
class="col col-xs-12 col-md-4"
>
{{toggleLabel}}
</div>
<div
class="col col-xs-12 col-md-2"
>
{{subscriberDisplayName}}
</div>
<div
class="col col-xs-12 col-md-6"
>
<q-field class="csc-cf-field-toggle">
<q-toggle
:value="primaryNumberEnabled"
:left-label="true"
@input="togglePrimaryNumber()"
/>
</q-field>
</div>
</div>
<div class="row">
<div
class="col col-xs-12 col-md-4"
>
<q-btn flat class="csc-cf-flat-btn">
+ {{ $t('pages.newCallForward.forwardBtnLabel') }}
</q-btn>
<q-popover
ref="destinationType"
anchor="center right"
>
<q-list
link
class="no-border"
>
<q-item>
<q-item-main
:label="this.$t('pages.newCallForward.numberLabel')"
@click="showForm()"
/>
</q-item>
<q-item>
<q-item-main
:label="this.$t('pages.newCallForward.voiceMailLabel')"
@click="addVoicemail()"
/>
</q-item>
</q-list>
</q-popover>
</div>
</div>
<div
class="row csc-cf-destinations-cont"
>
<div
class="col col-xs-12 col-md-12"
>
<csc-new-call-forward-destination
v-for="(destination, index) in destinations"
:key="index"
:index="index"
:destination="destination"
/>
</div>
</div>
<div
class="row"
>
<div
class="col col-xs-12 col-md-6"
>
<csc-new-call-forward-add-destination-form
class="csc-list-form col-xs-12 col-md-4 col-lg-6"
ref="addDestinationForm"
/>
</div>
</div>
</csc-page>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import {
QField,
QToggle,
QBtn,
QPopover,
QList,
QItem,
QItemMain
} from 'quasar-framework'
import CscPage from '../../CscPage'
import CscNewCallForwardDestination from './CscNewCallForwardDestination'
import CscNewCallForwardAddDestinationForm from './CscNewCallForwardAddDestinationForm'
export default {
components: {
CscPage,
CscNewCallForwardDestination,
CscNewCallForwardAddDestinationForm,
QField,
QToggle,
QBtn,
QPopover,
QList,
QItem,
QItemMain,
},
data () {
return {};
},
async mounted(){
await this.$store.dispatch('newCallForward/loadDestinationsets');
const unconditionalDestSet = await this.$store.dispatch('newCallForward/getDestinationSetByName', 'csc-unconditional');
if(unconditionalDestSet){
this.$store.dispatch('newCallForward/loadDestinations', unconditionalDestSet.destinations);
}
},
computed: {
...mapState('newCallForward', []),
...mapGetters('newCallForward', [
'subscriberDisplayName',
'destinations'
]),
primaryNumberEnabled(){
return true;
},
toggleLabel(){
return `${this.$t('pages.newCallForward.primarNumberEnabled')}`;
}
},
methods: {
togglePrimaryNumber(){},
showForm(){
this.$refs.destinationType.close();
this.$refs.addDestinationForm.add();
},
addVoicemail(){}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../../themes/app.common.styl'
.csc-cf-flat-btn
color $primary
float right
.csc-cf-destinations-cont
margin-top 25px
.csc-cf-field-toggle
margin-top 0px;
</style>

@ -0,0 +1,152 @@
<template>
<div
v-if="enabled"
class="csc-form"
>
<csc-new-call-forward-input
:label="$t('callBlocking.number')"
:prefilled="destination"
v-model="number"
@submit="save"
@error="error"
/>
<div
class="csc-form-actions row justify-center"
>
<q-btn
flat
color="default"
icon="clear"
@mousedown.native="cancel()"
>
{{ $t('buttons.cancel') }}
</q-btn>
<q-btn
v-if="!loading"
flat
color="primary"
icon="done"
@click="save(); close()"
:disable="saveDisabled"
>
{{ $t('buttons.save') }}
</q-btn>
<div
v-if="loading"
class="csc-form-actions-spinner"
>
<csc-spinner />
</div>
</div>
</div>
</template>
<script>
import CscNewCallForwardInput from './CscNewCallForwardInput'
import CscSpinner from '../../CscSpinner'
import {
showGlobalError
} from '../../../helpers/ui'
import {
maxLength
} from 'vuelidate/lib/validators'
import {
QField,
QInput,
QBtn
} from 'quasar-framework'
export default {
name: 'csc-new-call-forward-add-destination-form',
components: {
CscNewCallForwardInput,
CscSpinner,
QField,
QInput,
QBtn
},
data () {
return {
enabled: false,
number: '',
numberError: false,
destinationIndex: null
}
},
props: [
'destination',
'index',
'disable',
'loading',
],
validations: {
number: {
maxLength: maxLength(64)
}
},
updated(){
if(Number.isInteger(this.index)){
this.destinationIndex = this.index;
}
},
computed: {
saveDisabled() {
return this.numberError|| this.disable || this.loading;
}
},
methods: {
async save() {
const destinationSetName = 'csc-unconditional'; // gonna be dynamic
const getDestinationSetByName = await this.$store.dispatch('newCallForward/getDestinationSetByName', destinationSetName);
if (this.numberError || this.saveDisabled) {
showGlobalError(this.$t('validationErrors.generic'));
}
else if(Number.isInteger(this.destinationIndex)){ // edit mode
this.$store.dispatch('newCallForward/editDestination',{
index: this.destinationIndex,
destinationSetId: getDestinationSetByName.id,
destination: this.number
});
}
else { // new destination
let destinationSetId;
if(!getDestinationSetByName){
destinationSetId = await this.$store.dispatch('newCallForward/addDestinationSet', destinationSetName);
await this.$store.dispatch('newCallForward/loadDestinationsets'); // keeps local data updated
}
else{
destinationSetId = getDestinationSetByName.id;
}
this.$store.dispatch('newCallForward/addDestination', {
destinationSetId: destinationSetId,
destination: this.number
});
}
},
cancel() {
this.number = '';
this.enabled = false;
},
add() {
this.number = '';
this.enabled = true;
},
close() {
this.enabled = false;
},
reset() {
this.cancel();
},
error(state) {
this.numberError = state;
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../../themes/app.common.styl'
</style>

@ -0,0 +1,136 @@
<template>
<div class="row csc-cf-destination-cont">
<div class="col col-xs-12 col-md-4 ">
{{ $t('pages.newCallForward.destinationTimeoutLabel') }}
<span class='csc-cf-timeout'>
{{this.destinationTimeout}}
<q-popover
ref="timeoutForm"
self="top left"
class="csc-cf-timeout-form"
>
<q-slider
v-model="destinationTimeout"
label
label-always
:step="5"
:min="0"
:max="120"
@change="saveTimeout()"
/>
</q-popover>
</span>
{{ $t('pages.newCallForward.destinationNumberLabel') }}
</div>
<div class="col col-xs-12 col-md-2 ">
<span class='csc-cf-destination'>
{{this.destinationNumber}}
<q-popover
ref="numberForm"
anchor="top right"
class="csc-cf-number-form"
@open="showNumberForm()"
>
<csc-new-call-forward-add-destination-form
ref="addDestinationForm"
:index="this.destinationIndex"
:destination="this.destinationNumber"
/>
</q-popover>
</span>
</div>
<div class="col col-xs-12 col-md-2 ">
<!-- TODO add remove btn -->
</div>
</div>
</template>
<script>
import {
// QField,
// QToggle,
QBtn,
QPopover,
QSlider,
QList,
QItem,
QItemMain
} from 'quasar-framework'
import { mapGetters } from 'vuex'
import CscNewCallForwardAddDestinationForm from './CscNewCallForwardAddDestinationForm'
export default {
name: 'csc-new-call-forward-destination',
components: {
QBtn,
QPopover,
QSlider,
QList,
QItem,
QItemMain,
CscNewCallForwardAddDestinationForm
},
props: [
'destination',
'index'
],
mounted(){
this.updateValues(this.destination)
},
data(){
return {
destinationTimeout: 0,
destinationNumber: null,
destinationIndex: null
}
},
computed: {
...mapGetters('newCallForward', [
'destinations'
])
},
methods: {
updateValues(destination){
this.destinationTimeout = destination.timeout;
this.destinationNumber = destination.simple_destination;
this.destinationIndex = this.index;
},
showNumberForm(){
this.$refs.addDestinationForm.add();
},
async saveTimeout(){
const destinationSetName = 'csc-unconditional'; // gonna be dynamic
const getDestinationSetByName = await this.$store.dispatch('newCallForward/getDestinationSetByName', destinationSetName);
this.$store.dispatch('newCallForward/editTimeout', {
index: this.destinationIndex,
timeout: this.destinationTimeout,
destinationSetId: getDestinationSetByName.id
});
}
},
watch: {
destinations(){
if(Number.isInteger(this.destinationIndex)){
this.updateValues(this.destinations[this.destinationIndex])
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../../themes/app.common.styl'
.csc-cf-destination-cont
padding 5px
.csc-cf-timeout,
.csc-cf-destination
color $primary
cursor pointer
.csc-cf-timeout-form,
.csc-cf-number-form
min-width 120px
padding 0 15px 0 15px
</style>

@ -0,0 +1,109 @@
<template>
<q-field
:error-label="errorMessage"
>
<q-input
dark
clearable
type="text"
:float-label="label"
v-model="inputValue"
@keyup.enter="submit"
@keypress.space.prevent
@keydown.space.prevent
@keyup.space.prevent
@input="input"
@blur="blur"
:error="$v.inputValue.$error"
:before="beforeButtons"
/>
</q-field>
</template>
<script>
import {
QField,
QInput
} from 'quasar-framework'
import { isPhone } from '../../../helpers/validation'
import {
maxLength,
required
} from 'vuelidate/lib/validators'
export default {
name: 'csc-new-call-forward-input',
props: {
label: String,
prefilled: String,
before: Array
},
mounted(){
if(this.prefilled){
this.inputValue = this.prefilled;
}
},
data () {
return {
inputValue: '',
error: ''
}
},
validations: {
inputValue: {
isPhone,
maxLength: maxLength(64),
required
}
},
components: {
QField,
QInput
},
computed: {
errorMessage() {
if (!this.$v.inputValue.required) {
return this.$t('validationErrors.fieldRequired', {
field: this.label
});
}
else if (!this.$v.inputValue.maxLength) {
return this.$t('validationErrors.maxLength', {
field: this.label,
maxLength: this.$v.inputValue.$params.maxLength.max
});
}
else if (!this.$v.inputValue.isPhone) {
return this.$t('validationErrors.inputValidNumber');
}
},
beforeButtons() {
return this.before ? this.before : [];
}
},
methods: {
submit() {
this.$emit('submit');
},
input() {
this.$v.inputValue.$touch();
this.error = this.$v.inputValue.$error;
this.$emit('input', this.inputValue);
},
blur() {
this.$v.inputValue.$touch();
this.error = this.$v.inputValue.$error;
}
},
watch: {
error(state) {
this.$emit('error', state);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -1,64 +0,0 @@
<template>
<csc-page
class="csc-simple-page"
>
<div
class="row"
>
<div
class="col col-xs-12 col-md-6"
>
<q-field
class="csc-form-field"
>
<q-toggle
:label="toggleLabel"
:value="primaryNumberEnabled"
:left-label="true"
@input="togglePrimaryNumber()"
/>
</q-field>
</div>
</div>
</csc-page>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import {
QField,
QToggle
} from 'quasar-framework'
import CscPage from '../../CscPage'
export default {
data () {
return {};
},
components: {
CscPage,
QField,
QToggle
},
created() {},
computed: {
...mapState('newCallForward', []),
...mapGetters('newCallForward', [
'subscriberDisplayName'
]),
primaryNumberEnabled(){
return true;
},
toggleLabel(){
return `${this.$t('pages.newCallForward.primarNumberEnabled')} ${this.subscriberDisplayName}`;
}
},
methods: {
togglePrimaryNumber(){}
},
watch: {}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -16,3 +16,8 @@ export function userInfoAndEmpty(value) {
export function customMacAddress (value) {
return macAddressRegExp.test(value);
}
export function isPhone(value) {
return /^\+[0-9]?()[0-9](\s|\S)(\d[0-9]{9})$/.test(value);
}

@ -211,7 +211,12 @@
},
"newCallForward": {
"primarNumberEnabled": "All calls go to the primary number",
"primarNumberDisabled": "No call goes to"
"primarNumberDisabled": "No call goes to",
"forwardBtnLabel": "Add forwarding",
"numberLabel": "Number",
"voiceMailLabel": "Voicemail",
"destinationTimeoutLabel": "Then after ",
"destinationNumberLabel": "seconds forwarded to"
},
"callForward": {
"titles": {

@ -6,7 +6,7 @@ import ConferenceLayout from './components/layouts/Conference'
import DefaultLayout from './components/layouts/Default'
import Home from './components/pages/Home'
import Conversations from './components/pages/Conversations/Conversations'
import NewCallForward from './components/pages/NewCallForward/NewCallForward'
import CscNewCallForward from './components/pages/NewCallForward/CscNewCallForward'
import CallForwardAlways from './components/pages/CallForward/Always'
import CallForwardCompanyHours from './components/pages/CallForward/CompanyHours'
import CallForwardAfterHours from './components/pages/CallForward/AfterHours'
@ -48,7 +48,7 @@ export default [
},
{
path: 'new-call-forward',
component: NewCallForward
component: CscNewCallForward
},
{
path: 'call-forward/always',

@ -1,13 +1,142 @@
'use strict';
import Vue from 'vue'
// import _ from 'lodash';
// import { RequestState } from './common'
// import { i18n } from '../i18n';
import {
// getSourcesets,
getDestinationsets,
addNewDestinationsetWithName,
// deleteDestinationFromDestinationset,
addDestinationToDestinationset,
// addDestinationToEmptyGroup,
// addDestinationToExistingGroup,
// changePositionOfDestination,
// moveDestinationUp,
// moveDestinationDown,
// loadTimesetTimes,
// deleteTimeFromTimeset,
// deleteTimesetById,
// resetTimesetByName,
// createTimesetWithTime,
// appendTimeToTimeset,
// loadDestinations,
// createSourcesetWithSource,
// appendSourceToSourceset,
// deleteSourcesetById,
// deleteSourceFromSourcesetByIndex,
// flipCfuAndCft,
// getOwnPhoneTimeout,
// updateOwnPhoneTimeout
} from '../api/call-forward';
export default {
namespaced: true,
state: {
destinationsets:[],
destinations: [],
},
getters: {
subscriberDisplayName(state, getters, rootState, rootGetters) {
return rootGetters['user/getUsername'];
},
destinations(state) {
return state.destinations;
},
destinationsets(state){
return state.destinationsets;
}
},
mutations: {
addDestination(state, destination){
state.destinations.push(destination);
},
editDestination(state, data){
let destination = state.destinations.slice(data.index, data.index+1)[0];
destination.simple_destination = data.destination;
destination.destination = data.destination;
Vue.set(state.destinations, data.index, destination)
},
editTimeout(state, data){
let destination = state.destinations.slice(data.index, data.index+1)[0];
destination.timeout = data.timeout;
Vue.set(state.destinations, data.index, destination)
},
loadDestinationsets(state, destinationsets){
state.destinationsets = destinationsets;
},
loadDestinations(state, destinations){
state.destinations = destinations;
}
},
actions: {
async loadDestinationsets(context) {
try{
const destinationsets = await getDestinationsets(localStorage.getItem('subscriberId'));
context.commit('loadDestinationsets', destinationsets);
}
catch(err){
console.log(err)
}
},
mutations: {},
actions: {}
loadDestinations(context, destinations){
context.commit('loadDestinations', destinations);
},
async addDestinationSet(context, name) {
try{
const newDestinationset = await addNewDestinationsetWithName(name);
return newDestinationset;
}
catch(err){
console.log(err)
}
},
getDestinationSetByName(context, name){
let destinationsets = context.getters.destinationsets;
destinationsets = destinationsets.filter(($destinationset) => {
return $destinationset.name === name;
});
return destinationsets.length > 0 ? destinationsets[0] : null;
},
async addDestination(context, data){
try{
const destination = {
"announcement_id": null,
"simple_destination": data.destination,
"destination": data.destination,
"priority": 1,
"timeout": 10
};
await addDestinationToDestinationset({
id: data.destinationSetId,
data: [...context.state.destinations, destination]
});
context.commit('addDestination', destination);
}
catch(err){
console.log(err);
}
},
async editDestination(context, data){
let destination = context.state.destinations.slice(data.index, data.index+1)[0];
destination.simple_destination = data.destination;
destination.destination = data.destination;
context.commit('editDestination', data);
await addDestinationToDestinationset({
id: data.destinationSetId,
data: context.state.destinations
});
},
async editTimeout(context, data){
let destination = context.state.destinations.slice(data.index, data.index+1)[0];
destination.timeout = data.timeout;
context.commit('editTimeout', data);
await addDestinationToDestinationset({
id: data.destinationSetId,
data: context.state.destinations
});
}
}
};

Loading…
Cancel
Save