TT#23603 Call: As a Customer I want to input a number and start a call

Change-Id: Iaf1aa61fb0d5f7e0fe6ef4c427911a50933a09b8
changes/25/16525/5
Hans-Peter Herzog 8 years ago
parent a5d6d1bfb8
commit 6ab38fcbb7

@ -94,6 +94,35 @@ Now you can log in to csc with one of the normal subscriber you just created. UR
```https://<your-ip-address>/csc``` ```https://<your-ip-address>/csc```
### How to add new npm package
1. Ensure that you have a clean node_modules folder
```
rm -R node_modules/
npm install
```
1. Remove obsolete shrinkwrap file
```rm npm-shrinkwrap.json```
1. Install new package(s)
```npm install packageA packageB --save-dev```
1. Generate new shrinkwrap file including all dependencies
```npm shrinkwrap --dev```
You should see the following result in console:
```wrote npm-shrinkwrap.json```
1. Add new shrinkwrap file to git
```git add .```
## Contributing ## Contributing
See our [Contributing Guide](./CONTRIBUTING.md) file) for information on how to contribute. See our [Contributing Guide](./CONTRIBUTING.md) file) for information on how to contribute.

@ -39,6 +39,7 @@ webpackConfig.watchOptions = {
'debian', 'debian',
'dist', 'dist',
'node_modules', 'node_modules',
't',
'templates' 'templates'
] ]
} }

@ -35,16 +35,6 @@ module.exports = {
}, },
module: { module: {
rules: [ rules: [
// { // eslint
// enforce: 'pre',
// test: /\.(vue|js)$/,
// loader: 'eslint-loader',
// include: projectRoot,
// exclude: /node_modules/,
// options: {
// formatter: require('eslint-friendly-formatter')
// }
// },
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',

19468
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{ {
"name": "ngcp-csc-ui", "name": "ngcp-csc-ui",
"productName": "Customer Self-Care Web UI", "productName": "Customer Self-Care Web UI",
"version": "0.1.1", "version": "0.2.1",
"description": "Customer Self-Care Web UI", "description": "Customer Self-Care Web UI",
"author": "Hans-Peter Herzog <hherzog@sipwise.com>", "author": "Hans-Peter Herzog <hherzog@sipwise.com>",
"scripts": { "scripts": {
@ -9,25 +9,18 @@
"dev": "node build/script.dev.js", "dev": "node build/script.dev.js",
"build": "node build/script.build.js mat", "build": "node build/script.build.js mat",
"dev-build": "CSC_SOURCE_MAP=1 CSC_WATCH=1 node build/script.build.js mat", "dev-build": "CSC_SOURCE_MAP=1 CSC_WATCH=1 node build/script.build.js mat",
"lint": "eslint --ext .js,.vue src",
"test": "karma start ./karma.js --single-run" "test": "karma start ./karma.js --single-run"
}, },
"dependencies": { "dependencies": {
"ansi-html": "0.0.7",
"ansi-regex": "^3.0.0",
"babel-runtime": "^6.25.0", "babel-runtime": "^6.25.0",
"core-js": "^2.5.1",
"html-entities": "^1.2.1",
"lodash": "^4.17.4",
"quasar-extras": "0.x", "quasar-extras": "0.x",
"quasar-framework": "0.14.4", "quasar-framework": "^0.14.4",
"strip-ansi": "^4.0.0", "vue": "^2.5.0",
"vue": "2.3.4", "vue-i18n": "7.3.2",
"vue-i18n": "^7.3.0", "vue-resource": "1.3.4",
"vue-resource": "^1.3.4", "vue-router": "^2.5.0",
"vue-router": "^2.7.0", "vuex": "2.5.0",
"vuex": "^2.4.1", "vuex-router-sync": "4.3.2"
"vuex-router-sync": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
@ -57,6 +50,7 @@
"file-loader": "^0.11.1", "file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3", "friendly-errors-webpack-plugin": "^1.1.3",
"glob": "^7.1.2", "glob": "^7.1.2",
"google-libphonenumber": "3.0.7",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^2.30.1",
"http-proxy-middleware": "^0.17.0", "http-proxy-middleware": "^0.17.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
@ -67,6 +61,7 @@
"karma-mocha": "^1.3.0", "karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.4", "karma-webpack": "^2.0.4",
"load-script": "1.0.0", "load-script": "1.0.0",
"lodash": "4.17.4",
"mocha": "^4.0.0", "mocha": "^4.0.0",
"opn": "^5.0.0", "opn": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^3.2.0", "optimize-css-assets-webpack-plugin": "^3.2.0",
@ -79,7 +74,7 @@
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"vue-loader": "^13.0.5", "vue-loader": "^13.0.5",
"vue-style-loader": "^3.0.3", "vue-style-loader": "^3.0.3",
"vue-template-compiler": "~2.3.4", "vue-template-compiler": "^2.5.0",
"webpack": "^3.6.0", "webpack": "^3.6.0",
"webpack-dev-middleware": "^1.12.0", "webpack-dev-middleware": "^1.12.0",
"webpack-hot-middleware": "^2.19.1", "webpack-hot-middleware": "^2.19.1",

@ -0,0 +1,211 @@
<template>
<div class="csc-call">
<q-card flat color="secondary">
<q-card-title>
<q-icon v-if="isCalling && getMediaType == 'audioOnly'" name="mic" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && getMediaType == 'audioVideo'" name="videocam" color="primary" size="26px"/>
<q-icon v-else-if="isEnded" name="error" color="primary" size="26px"/>
<q-icon v-else name="call made" color="primary" size="26px"/>
<span v-if="isPreparing" class="text">{{ $t('call.startNew') }}</span>
<span v-else-if="isInitiating" class="text">{{ $t('call.initiating') }}</span>
<span v-else-if="isRinging" class="text">{{ $t('call.ringing') }}</span>
<span v-else-if="isEnded" class="text">{{ $t('call.ended') }}</span>
<span v-else class="text">{{ $t('call.call') }}</span>
<q-btn round small slot="right" class="no-shadow" @click="close()" icon="clear"/>
</q-card-title>
<q-card-main>
<div v-if="isTrying" class="csc-spinner"><q-spinner-rings color="primary" :size="60" /></div>
<div v-if="isCalling" class="phone-number">{{ getNumber }}</div>
<csc-media v-if="isCalling && localMediaStream != null" :stream="localMediaStream" />
<q-field v-if="isPreparing" :helper="$t('call.inputNumber')" :error="validationEnabled && phoneNumberError"
:error-label="$t('call.inputValidNumber')" :count="64" dark>
<q-input :float-label="$t('call.number')" v-model="formattedPhoneNumber" dark clearable max="64"
@blur="phoneNumberBlur()" @focus="phoneNumberFocus()"/>
</q-field>
<div v-if="isEnded" class="ended-reason">
{{ getEndedReason }}
</div>
</q-card-main>
<q-card-actions align="center">
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioOnly')" icon="mic" />
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioVideo')" icon="videocam" />
<q-btn v-if="isCalling" round small color="negative" @click="hangUp()" icon="call end" />
<q-btn v-if="isEnded" round small color="negative" @click="init()" icon="clear"/>
</q-card-actions>
</q-card>
</div>
</template>
<script>
import _ from 'lodash';
import { mapState, mapGetters } from 'vuex'
import CscMedia from './CscMedia'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput,
QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings } from 'quasar-framework'
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'
var phoneUtil = PhoneNumberUtil.getInstance();
export default {
name: 'csc-call',
props: ['region'],
data () {
return {
phoneNumber: '',
parsedPhoneNumber: null,
phoneNumberError: false,
validationEnabled: false
}
},
components: {
QLayout,
QCard,
QCardTitle,
QCardSeparator,
QCardMain,
QField,
QInput,
QCardActions,
QBtn,
QIcon,
QSpinnerRings,
CscMedia
},
methods: {
init() {
this.phoneNumber = '';
this.parsedPhoneNumber = null;
this.validationEnabled = false;
this.phoneNumberError = false;
this.$store.commit('call/inputNumber');
},
phoneNumberFocus() {
this.validationEnabled = true;
},
phoneNumberBlur() {
this.validationEnabled = false;
},
call(localMedia) {
this.validationEnabled = true;
if(this.parsedPhoneNumber !== null) {
this.phoneNumberError = false;
this.$store.dispatch('call/start', {
number: this.phoneNumber,
localMedia: localMedia
});
} else {
this.phoneNumberError = true;
}
},
hangUp() {
this.$store.dispatch('call/hangUp');
},
close() {
this.$store.commit('call/inputNumber');
this.$emit('close');
}
},
computed: {
formattedPhoneNumber: {
get() {
if(this.parsedPhoneNumber !== null) {
return _.trim(phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.INTERNATIONAL));
} else {
return _.trim(this.phoneNumber);
}
},
set(value) {
this.validationEnabled = true;
this.phoneNumber = _.trim(value);
if(this.phoneNumber.match('^[1-9]')) {
this.phoneNumber = '+' + this.phoneNumber;
} else if(this.phoneNumber === '+') {
this.phoneNumber = '';
}
if(phoneUtil.isPossibleNumberString(this.phoneNumber, this.region)) {
try {
this.parsedPhoneNumber = phoneUtil.parse(this.phoneNumber, this.region);
this.phoneNumber = phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.E164);
this.phoneNumberError = false;
} catch(err) {
this.parsedPhoneNumber = null;
this.phoneNumberError = true;
}
} else {
this.parsedPhoneNumber = null;
this.phoneNumberError = true;
}
}
},
localMediaStream() {
if(this.$store.state.call.localMediaStream != null) {
return this.$store.state.call.localMediaStream.getStream();
} else {
return null;
}
},
remoteMediaStream() {
console.log(this.$refs.remoteMedia);
},
...mapGetters('call', [
'isPreparing',
'isInitiating',
'isTrying',
'isRinging',
'isCalling',
'isEnded',
'getNumber',
'getMediaType',
'getLocalMediaType',
'getEndedReason'
]),
hasLocalVideo() {
return this.getLocalMediaType !== null && this.getLocalMediaType.match(/(v|V)ideo/) !== null;
}
}
}
</script>
<style lang="stylus">
.csc-call .q-card {
margin:0;
}
.csc-call .q-card-main {
padding: 0;
}
.csc-call .q-field {
margin: 0px;
padding-left: 16px;
padding-right: 16px;
}
.csc-call .q-card-actions {
padding: 16px;
}
.csc-spinner {
text-align: center;
margin-bottom: 16px;
}
.q-card-title .text {
color: #adb3b8;
}
.csc-call .phone-number {
font-size: 18px;
text-align: center;
color: #adb3b8;
margin-bottom: 16px;
}
.csc-call .ended-reason {
font-size: 18px;
text-align: center;
color: #adb3b8;
}
</style>

@ -0,0 +1,42 @@
<template>
<div class="csc-media">
<video ref="media"></video>
</div>
</template>
<script>
import _ from 'lodash';
export default {
name: 'csc-media',
props: ['stream'],
data () {
return {
currentStream: this.stream
}
},
mounted() {
if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.srcObject)) {
this.$refs.media.srcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.mozSrcObject)) {
this.$refs.media.mozSrcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
_.isObject(URL) && _.isFunction(URL.createObjectURL)) {
this.$refs.media.src = URL.createObjectURL(this.currentStream);
}
},
components: {},
methods: {},
computed: {}
}
</script>
<style lang="stylus">
.csc-media {
width: 100%
}
.csc-media video {
width: 100%
}
</style>

@ -10,9 +10,9 @@
</template> </template>
<script> <script>
import { QIcon, QFixedPosition } from 'quasar-framework' import { QIcon, QFixedPosition, QFab, QFabAction, QTooltip } from 'quasar-framework'
export default { export default {
name: 'page', name: 'csc-page',
props: [ props: [
'title' 'title'
], ],
@ -21,7 +21,10 @@
}, },
components: { components: {
QIcon, QIcon,
QFixedPosition QFixedPosition,
QFab,
QFabAction,
QTooltip
} }
} }
</script> </script>
@ -30,6 +33,7 @@
@import '../../src/themes/app.variables.styl'; @import '../../src/themes/app.variables.styl';
@import '../../src/themes/quasar.variables.styl'; @import '../../src/themes/quasar.variables.styl';
.page { .page {
position: relative;
padding: 60px; padding: 60px;
padding-top: 100px; padding-top: 100px;
} }
@ -42,7 +46,6 @@
} }
.page .page-title { .page .page-title {
right: 0;
padding: 30px; padding: 30px;
padding-left: 60px; padding-left: 60px;
padding-right: 60px; padding-right: 60px;
@ -50,6 +53,11 @@
z-index: 1000; z-index: 1000;
} }
.page .page-button {
padding-top: 20px;
padding-right: 60px;
}
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
.page { .page {
padding: 30px; padding: 30px;
@ -62,7 +70,6 @@
} }
.page .page-title { .page .page-title {
right: 0;
padding: 30px; padding: 30px;
background-color: white; background-color: white;
z-index: 1000; z-index: 1000;

@ -1,5 +1,5 @@
<template> <template>
<q-layout ref="layout" view="lHr LpR lFr" :right-breakpoint="1100"> <q-layout ref="layout" view="lHh LpR lFf" :right-breakpoint="1100">
<q-toolbar slot="header"> <q-toolbar slot="header">
<q-btn flat @click="$refs.layout.toggleLeft()"> <q-btn flat @click="$refs.layout.toggleLeft()">
<q-icon name="menu"/> <q-icon name="menu"/>
@ -82,8 +82,9 @@
</q-side-link> </q-side-link>
</q-collapsible> </q-collapsible>
</q-list> </q-list>
<div id="page-action-button"> <router-view />
<q-fab v-if="hasCommunicationCapabilities" <q-fixed-position corner="top-right" :offset="[20, 20]" class="page-button transition-generic">
<q-fab v-if=""
color="primary" icon="question answer" color="primary" icon="question answer"
active-icon="clear" direction="down" flat> active-icon="clear" direction="down" flat>
<q-fab-action v-if="hasFaxCapability" color="primary" @click="" icon="fa-fax"> <q-fab-action v-if="hasFaxCapability" color="primary" @click="" icon="fa-fax">
@ -92,12 +93,12 @@
<q-fab-action v-if="hasSmsCapability" color="primary" @click="" icon="fa-send"> <q-fab-action v-if="hasSmsCapability" color="primary" @click="" icon="fa-send">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip> <q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip>
</q-fab-action> </q-fab-action>
<q-fab-action v-if="isCallAvailable" color="primary" @click="" icon="fa-phone"> <q-fab-action v-if="isCallAvailable" color="primary" @click="call()" icon="fa-phone">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip> <q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip>
</q-fab-action> </q-fab-action>
</q-fab> </q-fab>
</div> </q-fixed-position>
<router-view /> <csc-call ref="cscCall" slot="right" @close="$refs.layout.hideRight()" region="DE" />
</q-layout> </q-layout>
</template> </template>
@ -105,6 +106,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError, showToast } from '../../helpers/ui' import { startLoading, stopLoading, showGlobalError, showToast } from '../../helpers/ui'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import CscCall from '../CscCall'
import { import {
QLayout, QLayout,
QToolbar, QToolbar,
@ -123,12 +125,13 @@
QTooltip, QTooltip,
QSideLink, QSideLink,
QTransition, QTransition,
QCollapsible QCollapsible,
} from 'quasar' } from 'quasar-framework'
export default { export default {
name: 'default', name: 'default',
mounted: function() { mounted: function() {
this.$refs.layout.showLeft(); this.$refs.layout.showLeft();
this.$refs.layout.hideRight();
if(!this.hasUser) { if(!this.hasUser) {
startLoading(); startLoading();
this.$store.dispatch('user/initUser').then(()=>{ this.$store.dispatch('user/initUser').then(()=>{
@ -159,7 +162,8 @@
QTooltip, QTooltip,
QSideLink, QSideLink,
QTransition, QTransition,
QCollapsible QCollapsible,
CscCall
}, },
computed: { computed: {
...mapGetters('call', [ ...mapGetters('call', [
@ -185,12 +189,17 @@
methods: { methods: {
showInitialToasts() { showInitialToasts() {
if(this.isCallAvailable) { if(this.isCallAvailable) {
showToast(this.$i18n.t('toasts.callIsAvailable')); showToast(this.$i18n.t('toasts.callAvailable'));
} }
if(this.hasCallInitFailure) { if(this.hasCallInitFailure) {
showToast(this.$i18n.t('toasts.callIsNotAvailable')); showToast(this.$i18n.t('toasts.callNotAvailable'));
} }
}, },
call() {
this.$refs.layout.showRight();
this.$refs.cscCall.init();
},
logout() { logout() {
startLoading(); startLoading();
this.$store.dispatch('user/logout').then(()=>{ this.$store.dispatch('user/logout').then(()=>{
@ -206,19 +215,6 @@
@import '../../../src/themes/app.variables.styl'; @import '../../../src/themes/app.variables.styl';
@import '../../../src/themes/quasar.variables.styl'; @import '../../../src/themes/quasar.variables.styl';
#page-action-button {
z-index: 1001;
position: fixed;
top: ($toolbar-min-height + 15)px;
right: 55px;
}
@media (max-width: $breakpoint-sm) {
#page-action-button {
right: 25px;
}
}
#main-menu { #main-menu {
padding-top:60px; padding-top:60px;
} }

@ -1,11 +1,11 @@
<template> <template>
<page :title="$t('pages.callBlocking' + suffix + '.title')"> <csc-page :title="$t('pages.callBlocking' + suffix + '.title')">
<q-field id="toggle-call-blocking"> <q-field id="toggle-call-blocking">
<csc-toggle :label="toggleButtonLabel" @change="toggle" :enabled="enabled"/> <csc-toggle :label="toggleButtonLabel" @change="toggle" :enabled="enabled"/>
</q-field> </q-field>
<div id="add-number-form"> <div id="add-number-form">
<q-field v-if="!addFormEnabled"> <q-field v-if="!addFormEnabled">
<q-btn color="primary" <q-btn flat color="primary"
icon="fa-plus" icon="fa-plus"
@click="enableAddForm()">{{ $t('pages.callBlocking' + suffix + '.addNumberButton') }}</q-btn> @click="enableAddForm()">{{ $t('pages.callBlocking' + suffix + '.addNumberButton') }}</q-btn>
</q-field> </q-field>
@ -13,37 +13,39 @@
<q-field :error="addFormError" :error-label="$t('pages.callBlocking' + suffix + '.addInputError')"> <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="Number" v-model="newNumber" clearable @keyup.enter="addNumber()" />
</q-field> </q-field>
<q-btn @click="disableAddForm()">{{ $t('buttons.cancel') }}</q-btn> <q-btn flat @click="disableAddForm()">{{ $t('buttons.cancel') }}</q-btn>
<q-btn color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn> <q-btn flat color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn>
</div> </div>
</div> </div>
<div>
<q-card class="blocked-number" v-for="(number, index) in numbers"> <q-card class="blocked-number" v-for="(number, index) in numbers">
<q-card-title> <q-card-title>
<q-icon v-if="!(editing && editingIndex == index)" name="fa-ban" color="secondary" size="22px"/>
<span v-if="!(editing && editingIndex == index)" @click="editNumber(index)">{{ number }}</span> <span 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="Number"
v-model="editingNumber" @keyup.enter="saveNumber(index)" /> v-model="editingNumber" @keyup.enter="saveNumber(index)" />
<q-icon v-if="editing && editingIndex == index" slot="right" <q-btn color="primary" flat small round v-if="editing && editingIndex == index" slot="right"
name="fa-save" @click="saveNumber(index)" class="cursor-pointer"></q-icon> icon="fa-save" @click="saveNumber(index)" class="cursor-pointer">{{ $t('buttons.save') }}</q-btn>
<q-icon v-if="!(editing && editingIndex == index)" slot="right" <q-btn color="primary" flat small round v-if="editing && editingIndex == index" slot="right"
name="fa-edit" @click="editNumber(index)" class="cursor-pointer"></q-icon> icon="clear" @click="saveNumber(index)" class="cursor-pointer">{{ $t('buttons.cancel') }}</q-btn>
<q-icon v-if="!(editing && editingIndex == index)" slot="right" <q-btn color="primary" flat small round v-if="!(editing && editingIndex == index)" slot="right"
name="fa-remove" @click="removeNumber(index)" class="cursor-pointer"></q-icon> icon="fa-edit" @click="editNumber(index)" class="cursor-pointer">{{ $t('buttons.edit') }}</q-btn>
<q-btn color="primary" flat small round v-if="!(editing && editingIndex == index)" slot="right"
icon="delete" @click="removeNumber(index)" class="cursor-pointer">{{ $t('buttons.remove') }}</q-btn>
</q-card-title> </q-card-title>
</q-card> </q-card>
<q-inner-loading :visible="listLoading"> <q-inner-loading :visible="listLoading">
<q-spinner-mat size="50px" color="primary"></q-spinner-mat> <q-spinner-mat size="50px" color="primary"></q-spinner-mat>
</q-inner-loading> </q-inner-loading>
</div> </div>
</page> </csc-page>
</template> </template>
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError, showToast } from '../../../helpers/ui' import { startLoading, stopLoading, showGlobalError, showToast } from '../../../helpers/ui'
import Page from '../../Page' import CscPage from '../../CscPage'
import CscToggle from '../../form/CscToggle' import CscToggle from '../../form/CscToggle'
import { QInput, QCard, QBtn, QField, QIcon, QCardTitle, Dialog, QSpinnerMat, QToggle, import { QInput, QCard, QBtn, QField, QIcon, QCardTitle, QCardActions, Dialog, QSpinnerMat, QToggle,
Toast, QList, QItem, QItemMain, QCardMain, QInnerLoading } from 'quasar-framework' Toast, QList, QItem, QItemMain, QCardMain, QInnerLoading } from 'quasar-framework'
export default { export default {
name: 'csc-call-blocking', name: 'csc-call-blocking',
@ -76,7 +78,6 @@
}); });
}, },
components: { components: {
Page,
QToggle, QToggle,
Toast, Toast,
QField, QField,
@ -89,10 +90,12 @@
QCardMain, QCardMain,
QIcon, QIcon,
QCardTitle, QCardTitle,
QCardActions,
Dialog, Dialog,
QInnerLoading, QInnerLoading,
QSpinnerMat, QSpinnerMat,
CscToggle CscToggle,
CscPage
}, },
computed: { computed: {
numbers (){ numbers (){
@ -210,9 +213,6 @@
#add-number-form { #add-number-form {
margin-bottom:15px; margin-bottom:15px;
} }
.blocked-number .q-card-title-extra .q-icon {
margin-left: 10px;
}
.blocked-number .q-input { .blocked-number .q-input {
margin:0; margin:0;
} }

@ -1,13 +1,13 @@
<template> <template>
<page title="Privacy"> <csc-page title="Privacy">
<q-toggle :label="(!callBlockingEnabled ? 'Hide' : 'Show') + ' Own Number'" <q-toggle :label="(!callBlockingEnabled ? 'Hide' : 'Show') + ' Own Number'"
@input="toggle()" v-model="callBlockingEnabled"/> @input="toggle()" v-model="callBlockingEnabled"/>
</page> </csc-page>
</template> </template>
<script> <script>
import { showToast } from '../../../helpers/ui' import { showToast } from '../../../helpers/ui'
import Page from '../../Page' import CscPage from '../../CscPage'
import { QField, QToggle, Toast } from 'quasar-framework' import { QField, QToggle, Toast } from 'quasar-framework'
export default { export default {
data () { data () {
@ -23,7 +23,7 @@
}); });
}, },
components: { components: {
Page, CscPage,
QToggle, QToggle,
Toast, Toast,
QField QField

@ -1,15 +1,15 @@
<template> <template>
<page title="After Hours"></page> <csc-page title="After Hours"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,15 +1,15 @@
<template> <template>
<page title="Always"></page> <csc-page title="Always"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,15 +1,15 @@
<template> <template>
<page title="Company Hours"></page> <csc-page title="Company Hours"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,15 +1,15 @@
<template> <template>
<page title="Conversations"></page> <csc-page title="Conversations"></csc-page>
</template> </template>
<script> <script>
import Page from '../Page' import CscPage from '../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,15 +1,15 @@
<template> <template>
<page title="Devices"></page> <csc-page title="Devices"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,17 +1,17 @@
<template> <template>
<page title="PBX Groups"></page> <csc-page title="PBX Groups"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
import { QChip, QCard, QCardSeparator, QCardTitle, QCardMain, import { QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
QIcon, QPopover, QList, QItem, QItemMain } from 'quasar-framework' QIcon, QPopover, QList, QItem, QItemMain } from 'quasar-framework'
import { mapState } from 'vuex' import { mapState } from 'vuex'
export default { export default {
components: { components: {
Page, CscPage,
QChip, QChip,
QCard, QCard,
QCardSeparator, QCardSeparator,

@ -1,15 +1,15 @@
<template> <template>
<page title="Seats"></page> <csc-page title="Seats"></csc-page>
</template> </template>
<script> <script>
import Page from '../../Page' import CscPage from '../../CscPage'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
Page CscPage
} }
} }
</script> </script>

@ -1,19 +1,19 @@
<template> <template>
<page :title="$t('pages.reminder.title')"> <csc-page :title="$t('pages.reminder.title')">
<q-field class="reminder-field"> <q-field class="reminder-field">
<q-toggle :label="$t('pages.reminder.title') + (active ? ' enabled':' disabled')" @input="toggleReminder()" v-model="active" /> <q-toggle :label="$t('pages.reminder.title') + (active ? ' enabled':' disabled')" @input="toggleReminder()" v-model="active" />
</q-field> </q-field>
<q-field class="reminder-field"> <q-field class="reminder-field">
<q-datetime type="time" :disable="!active" no-clear=true v-model="timeConverted" :placeholder="$t('reminder.timeLabel')" @change="changeTime()" /> <q-datetime type="time" :disable="!active" no-clear=true v-model="timeConverted" :placeholder="$t('reminder.timeLabel')" @change="changeTime()" />
</q-field> </q-field>
<q-field class="reminder-field"> <q-field class="reminder-field">
<q-option-group :disable="!active" color="positive" type="radio" v-model="recurrence" @change="changeRecurrence()" :options="[ <q-option-group :disable="!active" color="positive" type="radio" v-model="recurrence" @change="changeRecurrence()" :options="[
{ label: $t('pages.reminder.recurrence.once'), value: 'never' }, { label: $t('pages.reminder.recurrence.once'), value: 'never' },
{ label: $t('pages.reminder.recurrence.weekdays'), value: 'weekdays' }, { label: $t('pages.reminder.recurrence.weekdays'), value: 'weekdays' },
{ label: $t('pages.reminder.recurrence.always'), value: 'always' } { label: $t('pages.reminder.recurrence.always'), value: 'always' }
]" /> ]" />
</q-field> </q-field>
</page> </csc-page>
</template> </template>
<script> <script>
@ -22,7 +22,7 @@ import {
stopLoading, stopLoading,
showGlobalError showGlobalError
} from '../../helpers/ui' } from '../../helpers/ui'
import Page from '../Page' import CscPage from '../CscPage'
import { import {
QField, QField,
QToggle, QToggle,
@ -49,7 +49,7 @@ export default {
}); });
}, },
components: { components: {
Page, CscPage,
QToggle, QToggle,
Toast, Toast,
QDatetime, QDatetime,

@ -8,7 +8,12 @@
"buttons": { "buttons": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"remove": "Remove" "remove": "Remove",
"edit": "Edit"
},
"toasts": {
"callAvailable": "You are now able to start and receive calls",
"callNotAvailable": "Could not initialize call functionality properly"
}, },
"navigation": { "navigation": {
"conversations": { "conversations": {
@ -88,8 +93,14 @@
"recurrenceUpdatedMsg": "Recurrence updated!" "recurrenceUpdatedMsg": "Recurrence updated!"
} }
}, },
"toasts": { "call": {
"callIsAvailable:": "You are now able to start and receive calls", "startNew": "Start new call",
"callIsNotAvailable": "Could not initialize call functionality properly" "initiating": "Call Initiating ...",
"ringing": "Call Ringing ...",
"ended": "Call ended",
"call": "Call",
"inputNumber": "Input a phone number",
"inputValidNumber": "Input a valid phone number",
"number": "Number"
} }
} }

@ -18,13 +18,14 @@ import { store } from './store'
import { i18n, locales } from './i18n' import { i18n, locales } from './i18n'
import router from './router' import router from './router'
import { sync } from 'vuex-router-sync' import { sync } from 'vuex-router-sync'
import filters from './filters' import { RtcEngineCall } from './plugins/call'
Vue.use(VueResource); Vue.use(VueResource);
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(Quasar); // Install Quasar Framework Vue.use(Quasar); // Install Quasar Framework
Vue.use(RtcEngineCall);
if (__THEME === 'mat') { if (__THEME === 'mat') {
require('quasar-extras/roboto-font') require('quasar-extras/roboto-font')

@ -0,0 +1,154 @@
import _ from 'lodash';
import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
import { createSessionToken } from '../api/rtcsession';
export const LocalMedia = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
videoOnly: 'videoOnly',
audioScreen: 'audioScreen',
screenOnly: 'screenOnly'
};
export class NetworkNotConnected extends Error {
constructor(network) {
super();
this.name = this.constructor.name;
this.message = 'Network ' + network + ' is not connected';
this.network = network;
}
}
var rtcEngineCallInstance = null;
export class RtcEngineCall {
constructor(options) {
this.networkTag = 'sip';
this.client = null;
this.network = null;
this.currentCall = null;
this.loadedLibrary = null;
this.sessionToken = null;
}
initialize() {
return new Promise((resolve, reject)=>{
Promise.resolve().then(($loadedLibrary)=>{
this.loadedLibrary = $loadedLibrary;
return this.loadLibrary();
}).then(()=>{
return this.createSession();
}).then(($sessionToken)=>{
this.sessionToken = $sessionToken;
return this.connectNetwork($sessionToken);
}).then(($network)=>{
this.network = $network;
resolve();
}).catch((err)=>{
reject(err);
});
});
}
isAvailable() {
return this.network !== null && this.network.isConnected;
}
hasRunningCall() {
return this.currentCall !== null;
}
loadLibrary() {
return loadCdkLib();
}
createSession() {
return createSessionToken();
}
connectNetwork(session) {
return connectDefaultCdkNetwork(session);
}
createLocalMedia(localMedia) {
return new Promise((resolve, reject)=>{
var localMediaStream = new cdk.LocalMediaStream();
var hasAudio = localMedia === LocalMedia.audioOnly ||
localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.audioScreen;
var hasVideo = localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.videoOnly;
var hasScreen = localMedia === LocalMedia.audioScreen ||
localMedia === LocalMedia.screenOnly;
localMediaStream.queryMediaSources((sources) => {
if (hasAudio && _.isObject(sources.defaultAudio)) {
localMediaStream.setAudio(sources.defaultAudio);
}
if (hasVideo && _.isObject(sources.defaultVideo)) {
localMediaStream.setVideo(sources.defaultVideo);
} else if (hasScreen && _.isObject(sources.desktopSharing)) {
localMediaStream.setVideo(sources.desktopSharing);
}
});
localMediaStream.build((err)=>{
if(_.isObject(err)) {
reject(err);
} else {
resolve(localMediaStream);
}
});
});
}
start(peer, localMediaStream) {
peer = peer.replace('+', '');
if(this.network !== null) {
this.currentCall = this.network.call(peer, { localMediaStream: localMediaStream });
return this.currentCall;
} else {
throw new NetworkNotConnected(this.networkTag);
}
}
onIncoming(listener) {
if(this.network !== null) {
this.network.onIncomingCall((call)=>{
if(this.currentCall === null) {
this.currentCall = call;
listener(call);
}
});
} else {
throw new NetworkNotConnected(this.networkTag);
}
}
accept(localMediaStream) {
if(this.currentCall !== null) {
this.currentCall.accept({
localMediaStream: localMediaStream
});
}
}
hangUp() {
if(this.currentCall !== null) {
this.currentCall.end();
}
}
static getInstance() {
if(rtcEngineCallInstance === null) {
rtcEngineCallInstance = new RtcEngineCall();
}
return rtcEngineCallInstance;
}
static install(Vue, options) {
Vue.call = RtcEngineCall.getInstance();
}
}

@ -1,98 +1,213 @@
'use strict'; 'use strict';
import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib'; // import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
import { createSessionToken } from '../api/rtcsession'; // import { createSessionToken } from '../api/rtcsession';
var cdkNetwork = null; import Vue from 'vue';
export var CallState = {
input: 'input',
initiating: 'initiating',
ringing: 'ringing',
incoming: 'incoming',
established: 'established',
ended: 'ended'
};
export var MediaType = {
audio: 'audio',
audioVideo: 'audioVideo',
audioScreen: 'audioScreen'
};
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
loaded: false, initialized: false,
initFailure: false, initError: null,
connected: false, endedReason: null,
disconnectReason: '', callState: CallState.input,
incoming: false, number: null,
incomingNumber: '', mediaType: null,
outgoing: false, localMediaType: null,
outgoingNumber: '' localMediaStream: null,
remoteMediaStream: null
}, },
getters: { getters: {
getNumber(state, getters) {
return state.number;
},
getMediaType(state, getters) {
return state.mediaType;
},
getLocalMediaType(state, getters) {
return state.localMediaType
},
getEndedReason(state, getters) {
return state.endedReason;
},
isNetworkConnected(state, getters) {
return state.initialized;
},
isCallAvailable(state, getters) { isCallAvailable(state, getters) {
return state.loaded && state.connected; return getters.isNetworkConnected;
}, },
hasCallInitFailure(state, getters) { hasCallInitFailure(state, getters) {
return state.initFailure; return state.initError !== null;
},
isPreparing(state, getters) {
return state.callState === CallState.input;
},
isInitiating(state, getters) {
return state.callState === CallState.initiating;
},
isTrying(state, getters) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing;
},
isRinging(state, getters) {
return state.callState === CallState.ringing;
},
isCalling(state, getters) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.established;
},
isEnded(state, getters) {
return state.callState === CallState.ended;
} }
}, },
mutations: { mutations: {
load(state) { initSucceeded(state) {
state.loaded = true; state.initialized = true;
state.initError = null;
}, },
initFailure(state) { initFailed(state, err) {
state.initFailure = true; state.initialized = false;
state.initError = err;
}, },
connect(state) { inputNumber(state) {
state.connected = true; state.callState = CallState.input;
}, },
disconnect(state, reason) { startCalling(state, options) {
state.connected = false; state.number = options.number;
state.disconnectReason = reason; state.mediaType = options.mediaType;
state.localMediaType = state.mediaType;
state.localMediaStream = options.localMediaStream;
state.callState = CallState.initiating;
}, },
incoming(state) { acceptIncoming(state, options) {
state.localMediaStream = options.localMediaStream;
}, },
outgoing(state) { startRinging(state) {
state.callState = CallState.ringing;
},
establishCall(state, options) {
state.remoteMediaStream = options.remoteMediaStream;
state.callState = CallState.established;
},
incomingCall(state, options) {
state.callState = CallState.incoming;
},
hangUpCall(state) {
state.callState = CallState.input;
if(_.isObject(state.localMediaStream)) {
state.localMediaStream.stop();
state.localMediaStream = null;
}
if(_.isObject(state.remoteMediaStream)) {
state.remoteMediaStream.stop();
state.remoteMediaStream = null;
}
},
endCall(state, reason) {
state.callState = CallState.ended;
state.endedReason = reason;
if(_.isObject(state.localMediaStream)) {
state.localMediaStream.stop();
state.localMediaStream = null;
}
if(_.isObject(state.remoteMediaStream)) {
state.remoteMediaStream.stop();
state.remoteMediaStream = null;
}
} }
}, },
actions: { actions: {
initialize(context) { initialize(context) {
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject)=>{
if(context.rootState.user.capabilities.rtcengine) { Vue.call.initialize().then(()=>{
loadCdkLib().then((script)=>{ context.commit('initSucceeded');
context.commit('load'); Vue.call.onIncoming((call)=>{
return createSessionToken(); context.commit('incomingCall');
}).then((sessionToken)=>{ call.onRemoteMedia((remoteMediaStream)=>{
return connectDefaultCdkNetwork(sessionToken); context.commit('establishCall', {
}).then(($cdkNetwork)=>{ remoteMediaStream: remoteMediaStream
cdkNetwork = $cdkNetwork; });
cdkNetwork.getClient().onConnect(()=>{ }).onRemoteMediaEnded(()=>{
context.commit('connect'); context.commit("endRemoteMedia");
}).onEnded(()=>{
context.commit('endCall', call.endedReason);
}); });
cdkNetwork.getClient().onDisconnect(()=>{
context.commit('disconnect', cdkNetwork.disconnectReason);
});
context.commit('connect');
resolve();
}).catch((err)=>{
context.commit('initFailure');
resolve();
}); });
} else {
resolve(); resolve();
} }).catch((err)=>{
context.commit('initFailed', err);
reject(err);
});
}); });
}, },
call(context) { /**
* @param context
}, * @param options
connect(context, sessionToken) { * @param options.localMedia
}, * @param options.number
disconnect(context) { */
start(context, options) {
}, Vue.call.createLocalMedia(options.localMedia).then((localMediaStream)=>{
enableAudio(context) { var call = Vue.call.start(options.number, localMediaStream);
call.onAccepted(()=>{
}, }).onEnded(()=>{
disableAudio(context) { context.commit('endCall', call.endedReason);
}).onPending(()=>{
context.commit('startCalling', {
number: options.number,
mediaType: options.localMedia,
localMediaStream: localMediaStream
});
}).onRemoteMedia((remoteMediaStream)=>{
context.commit('establishCall', {
remoteMediaStream: remoteMediaStream
});
}).onRemoteMediaEnded(()=>{
context.commit("endRemoteMedia");
}).onRingingStart(()=>{
context.commit('startRinging');
}).onRingingStop(()=>{
context.commit('stopRinging');
});
}).catch((err)=>{
context.commit('endCall', err.name);
});
}, },
enableVideo(context) { accept(context, localMedia) {
Vue.call.createLocalMedia(localMedia).then((localMediaStream)=>{
Vue.call.accept(localMediaStream);
context.commit('acceptIncoming', {
localMediaStream: localMediaStream
});
}).catch((err)=>{
context.commit('endCall', 'localMediaError');
});
}, },
disableVideo(context) { hangUp(context) {
if(Vue.call.hasRunningCall()) {
Vue.call.hangUp();
context.commit('hangUpCall');
} else {
context.commit('endCall', 'noRunningCall');
}
} }
} }
}; };

@ -90,9 +90,11 @@ export default {
subscriber: result.subscriber, subscriber: result.subscriber,
capabilities: result.capabilities capabilities: result.capabilities
}); });
return context.dispatch('call/initialize', null, { root: true }); context.dispatch('call/initialize', null, { root: true }).then(()=>{
}).then(()=>{ resolve();
resolve(); }).catch((err)=>{
resolve();
});
}).catch((err)=>{ }).catch((err)=>{
reject(err); reject(err);
}); });

@ -17,8 +17,8 @@
// to match your app's branding. // to match your app's branding.
$primary = #66A648 $primary = #66A648
$secondary = #26A69A $secondary = #32404E
$tertiary = #555 $tertiary = $secondary
$neutral = #E0E1E2 $neutral = #E0E1E2
$positive = #21BA45 $positive = #21BA45
@ -26,15 +26,14 @@ $negative = #DB2828
$info = #31CCEC $info = #31CCEC
$warning = #F2C037 $warning = #F2C037
$toolbar-background = #66A648 $toolbar-background = $primary
$toolbar-min-height = 60px $toolbar-min-height = 60px
$layout-header-shadow = $no-shadow $layout-header-shadow = $no-shadow
$layout-aside-background = $tertiary
$layout-aside-shadow = $no-shadow $layout-aside-shadow = $no-shadow
$layout-aside-left-width = 260px $layout-aside-left-width = 260px
$layout-aside-background = #32404E $layout-aside-right-width = 320px
$layout-footer-shadow = $no-shadow $layout-footer-shadow = $no-shadow
$tooltip-background = $primary $tooltip-background = $primary

Loading…
Cancel
Save