TT#23813 Implement minimal card component

What has been done:
- TT#23719, Conversations: Discover the conversations endpoint and
investigate dev tests
- TT#23812, Conversations: Implement store with needed states
- TT#23813, Conversations: Implement minimal card component
- TT#23814, Conversations: Implement store and api tests
- TT#23815, Conversations: Implement action and api request

Change-Id: I40231aded1309695d9a2ab15e196db7c83b62018
changes/36/16436/16
raxelsen 8 years ago
parent 6ab38fcbb7
commit 256cd94cb5

@ -5,11 +5,12 @@ module.exports = {
sourceType: 'module'
},
env: {
browser: true
browser: true,
{ "es6": true }
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'standard'
'eslint:recommended'
],
// required to lint *.vue files
plugins: [
@ -34,6 +35,7 @@ module.exports = {
'import/export': 2,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }]
'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }],
"no-console": 0
}
}

@ -0,0 +1,141 @@
# CONVERSATIONS MODULE
IMPORTANT: This document is meant for internal use at Sipwise, as it requires a vagrant-ngcp environment to be able to generate the test data.
Here are details on how to create test data for the /api/conversations endpoint that the conversations module is consuming.
## VOICEMAIL
1. Go to Settings > Subscribers > Details > Preferences, and under Call Forwards create a call forward destination under "Call Forward Unconditional" to voicemail
1. Initiate a call from another subscriber to the subscriber you just set call forward for, and leave a voicemail
See [calls section](#calls) for information on how to initiate calls.
## SMS
1. SSH in to the vagrant box, become root, and edit config.yml, set sms: enable: yes, and enter sms credentials for all affected fields:
[https://wiki.sipwise.com/wiki/index.php/SMS](https://wiki.sipwise.com/wiki/index.php/SMS)
1. ngcpcfg apply 'commit msg'
1. Go to Settings > Subscribers, find subscriber you want to use, and click "Details"
1. Copy one of the numbers from wiki (link in step 1 above) and add to E164 Number field
1. Send sms from a mobile phone to E164 number
1. To troubleshoot you can check log:
`tail -f /var/log/kannel/sms*`
For outgoing you need to use the api (also with E 164 number set to sms number from wiki). According to gjungwirth pretty simple with POST request containing the destination number and the content. 
## FAX
1. Go to Settings > Subscribers, find subscriber you want to use as caller, and click "Details"
1. Under "Master Data" click edit, and enter subscribers number also in the E164 field
1. Repeat the two steps above, this time for the callee
1. SSH in to the vagrant box, become root, and create a text file to use for sending fax:
`echo 'test test' > filetosend.txt`
1. Use the ngcp-fax tool to send a fax:
`ngcp-fax --caller 43993003 --callee 43993002 --file filetosend.txt`
1. To troubleshoot you can check log:
`tail -f /var/log/ngcp/faxserver.log`
## CALLS
Can be created by calling from one subscriber to another using the ngcp-csc-ui call (rtc:engine) feature.
Another option is to use a soft phone client such as linphone for desktop and mobile. Simply enter the subscriber username and password, and in the domain field enter the ip of the vagrant box you're using.
## XMPP CHAT
Should be created using an xmpp client authenticating as subscribers, but have not been able to verify this
## MOCK DATA
You can also use mock data in src/api/conversations.js, replacing the resolve() with this:
`
resolve(
{
"_embedded": {
"ngcp:conversations": [
{
"_links": {
},
"call_id": "SZ8e64JkCq",
"call_type": "call",
"callee": "43993006",
"caller": "43993007",
"direction": "out",
"duration": "0:00:08.592",
"id": 3,
"rating_status": "ok",
"start_time": "2017-11-07 14:19:00.526",
"status": "ok",
"type": "call"
},
{
"_links": {
},
"callee": "43993006",
"caller": "43993007",
"filename": "2cfb4c14-1958-472e-87a4-731ce5233514.tif",
"id": 1,
"pages": 0,
"start_time": "2017-11-07 10:46:11",
"status": "FAILED",
"subscriber_id": 235,
"type": "fax"
},
{
"_links": {
},
"callee": "43993006",
"caller": "43993007",
"filename": "264ba55c-e23c-4d41-8493-f096bb98add9.tif",
"id": 3,
"pages": 0,
"start_time": "2017-11-07 10:49:23",
"status": "FAILED",
"subscriber_id": 235,
"type": "fax"
},
{
"_links": {
},
"call_type": "cfu",
"callee": "43993007",
"caller": "vmu43993006@voicebox.local",
"direction": "out",
"duration": "0:00:06.854",
"id": 4,
"rating_status": "ok",
"start_time": "2017-11-07 15:21:55",
"status": "ok",
"type": "call"
},
]
},
"_links": {
"curies": {
"href": "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
"name": "ngcp",
"templated": true
},
"ngcp:conversations": [
{
"href": "/api/conversations/4?type=call"
},
{
"href": "/api/conversations/1?type=fax"
},
{
"href": "/api/conversations/4?type=fax"
}
],
"profile": {
"href": "http://purl.org/sipwise/ngcp-api/"
},
"self": {
"href": "/api/conversations/?page=1&rows=10"
}
},
"total_count": 4
}
);
`

@ -0,0 +1,13 @@
import Vue from 'vue';
import { getJsonBody } from './utils'
export function getConversations(id) {
return new Promise((resolve, reject) => {
Vue.http.get('/api/conversations/?subscriber_id=' + id).then((result)=>{
resolve(getJsonBody(result.body)._embedded['ngcp:conversations']);
}).catch((err)=>{
reject(err);
});
});
}

@ -0,0 +1,37 @@
<template>
<div class="csc-collapsible">
<q-collapsible :icon="icon"
:label="label"
:sublabel="sublabel">
</q-collapsible>
</div>
</template>
<script>
import { QCollapsible } from 'quasar-framework'
export default {
name: 'csc-collapsible',
props: [
'icon',
'label',
'sublabel'
],
components: {
QCollapsible
}
}
</script>
<style lang="stylus">
@import '~variables'
.csc-collapsible
.q-collapsible
.q-item-icon
font-size 22px
padding-right 12px
color $secondary
.q-item-label
color black
font-size 18px
font-weight 400
</style>

@ -5,7 +5,7 @@
</q-field>
<div id="add-number-form">
<q-field v-if="!addFormEnabled">
<q-btn flat color="primary"
<q-btn color="primary"
icon="fa-plus"
@click="enableAddForm()">{{ $t('pages.callBlocking' + suffix + '.addNumberButton') }}</q-btn>
</q-field>
@ -17,6 +17,7 @@
<q-btn flat color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn>
</div>
</div>
<div>
<q-card class="blocked-number" v-for="(number, index) in numbers">
<q-card-title>
<q-icon v-if="!(editing && editingIndex == index)" name="fa-ban" color="secondary" size="22px"/>

@ -1,18 +1,88 @@
<template>
<csc-page title="Conversations"></csc-page>
<csc-page :title="$t('pages.conversations.title')">
<q-card v-for="conversation in conversations" :key="conversation.caller"
class="conversation-card">
<csc-collapsible :icon="getCollapsibleIcons(conversation)"
:label="getCollapsibleLabel(conversation)"
:sublabel="conversation.start_time | readableDate">
</csc-collapsible>
<div v-if="hasCallOption(conversation.type)">
<q-card-separator />
<q-card-actions align="center">
<q-btn flat round small color="primary" icon="call">{{ $t('pages.conversations.buttons.call') }}</q-btn>
</q-card-actions>
</div>
</q-card>
</csc-page>
</template>
<script>
import CscPage from '../CscPage'
import CscCollapsible from '../card/CscCollapsible'
import { QBtn, QCardActions, QCard,
QCardSeparator } from 'quasar-framework'
export default {
data () {
return {}
return {
conversations: []
}
},
mounted() {
this.$store.dispatch('conversations/loadConversations').then(() => {
this.conversations = this.$store.state.conversations.
conversations;
}).catch((err) => {
console.log(err);
});
},
components: {
CscPage
CscPage,
QBtn,
QCard,
QCardActions,
QCardSeparator,
CscCollapsible
},
methods: {
hasCallOption(type) {
return (['call', 'call forward', 'sms', 'voicemail']
.indexOf(type) > -1);
},
getCollapsibleIcons(item) {
let directionIcon = item.direction == 'out' ? 'call_made' :
'call_received';
switch (item.type) {
case 'call':
return 'phone ' + directionIcon;
break;
case 'call forward':
return 'call_merge ' + directionIcon;
break;
case 'voicemail':
return 'voicemail ' + directionIcon;
break;
case 'fax':
return 'insert_drive_file ' + directionIcon;
break;
case 'sms':
return 'txtsms ' + directionIcon;
break;
};
},
getCollapsibleLabel(item) {
let prefix = item.status == 'ok' ? this.$t('pages.conversations.labels.successful')
: this.$t('pages.conversations.labels.unsuccessful');
let direction = item.direction == 'in' ? this.$t('pages.conversations.labels.from') : this.$t('pages.conversations.labels.to');
return `${prefix} ${item.type} ${direction} ${item.caller}`;
}
}
}
</script>
<style>
<style lang="stylus">
@import '~variables'
.conversation-card
padding 15px
.q-btn
margin-bottom -10px
</style>

@ -0,0 +1,7 @@
import { date } from 'quasar'
const { formatDate } = date
export default function(value) {
var timeStamp = new Date(value);
return `${formatDate(timeStamp, 'MMMM D, YYYY')} at ${formatDate(timeStamp, 'h:mm a')}`;
}

@ -1,5 +1,7 @@
import Vue from 'vue';
import NumberFilter from './number'
import DateFilter from './date'
Vue.filter('number', NumberFilter);
Vue.filter('readableDate', DateFilter);

@ -1,5 +1,4 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'

@ -5,9 +5,8 @@
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<title>Quasar App</title>
<link rel="icon" href="statics/quasar-logo.png" type="image/x-icon">
<title>Sipwise Customer Self Care</title>
<link rel="icon" href="statics/favicon.ico" type="image/x-icon">
</head>
<body>
<div id="q-app"></div>

@ -81,6 +81,18 @@
"addInputError": "Input a valid number or subscriber name",
"addNumberButton": "Add number"
},
"conversations": {
"title": "Conversations",
"labels": {
"successful": "Successful",
"unsuccessful": "Unsuccessful",
"from": "from",
"to": "to"
},
"buttons": {
"call": "CALL"
}
},
"reminder": {
"title": "Reminder",
"timeLabel": "Time of the day",

@ -19,6 +19,7 @@ import { i18n, locales } from './i18n'
import router from './router'
import { sync } from 'vuex-router-sync'
import { RtcEngineCall } from './plugins/call'
import filter from './filters'
Vue.use(VueResource);
@ -31,7 +32,6 @@ if (__THEME === 'mat') {
require('quasar-extras/roboto-font')
}
import 'quasar-extras/material-icons'
// import 'quasar-extras/ionicons'
import 'quasar-extras/fontawesome'
import 'quasar-extras/animate'

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

@ -0,0 +1,41 @@
'use strict';
import _ from 'lodash';
import { getConversations } from '../api/conversations';
export default {
namespaced: true,
state: {
conversations: [
]
},
mutations: {
loadConversations(state, options) {
let list = [];
_.forEach(options, function(item) {
delete item._links;
if (item.type == 'call') {
item.type = item.call_type != 'call' ? 'call forward'
: item.type;
};
list.push(item);
})
state.conversations = list;
}
},
actions: {
loadConversations(context) {
return new Promise((resolve, reject)=>{
getConversations(localStorage.getItem('subscriberId'))
.then((result)=>{
context.commit('loadConversations', result);
resolve();
})
.catch((err)=>{
reject(err);
});
});
}
}
};

@ -8,6 +8,7 @@ import PbxGroupsModule from './pbx-groups'
import CallBlockingModule from './call-blocking'
import ReminderModule from './reminder'
import CallModule from './call'
import ConversationsModule from './conversations'
Vue.use(Vuex);
@ -17,6 +18,7 @@ export const store = new Vuex.Store({
pbxGroups: PbxGroupsModule,
callBlocking: CallBlockingModule,
reminder: ReminderModule,
call: CallModule
call: CallModule,
conversations: ConversationsModule
}
});

@ -0,0 +1,72 @@
'use strict';
import Vue from 'vue';
import VueResource from 'vue-resource';
import { getConversations } from '../../src/api/conversations';
import { assert } from 'chai';
Vue.use(VueResource);
describe('Conversations', function(){
const subscriberId = 123;
it('should get all data regarding conversations', function(done){
let innerData = [{
"_links" : {
"collection" : {
"href" : "/api/conversations/"
},
"curies" : {
"href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
"name" : "ngcp",
"templated" : true
},
"ngcp:calls" : {
"href" : "/api/calls/5"
},
"ngcp:conversations" : {
"href" : "/api/conversations/5?type=call"
},
"profile" : {
"href" : "http://purl.org/sipwise/ngcp-api/"
},
"self" : {
"href" : "/api/conversations/5?type=call"
}
},
"call_id" : "cT1miqD5Nw",
"call_type" : "cfu",
"callee" : "vmu43993006@voicebox.local",
"caller" : "43993006",
"direction" : "out",
"duration" : "0:00:19.672",
"id" : 5,
"rating_status" : "ok",
"start_time" : "2017-11-10 08:51:10.452",
"status" : "ok",
"type" : "call"
}];
let data = {
"_embedded": {
"ngcp:conversations": innerData
}
};
Vue.http.interceptors = [];
Vue.http.interceptors.unshift((request, next)=>{
next(request.respondWith(JSON.stringify(data), {
status: 200
}));
});
getConversations(subscriberId).then((result)=>{
assert.deepEqual(result, innerData);
done();
}).catch((err)=>{
done(err);
});
});
});

@ -1,3 +1,4 @@
'use strict';
import Vue from 'vue';
@ -7,9 +8,10 @@ import { assert } from 'chai';
Vue.use(VueResource);
describe('Subscriber', function(){
const subscriberId = 123;
it('should get all subscriber preferences', function(done) {
Vue.http.interceptors = [];
Vue.http.interceptors.unshift((request, next)=>{
@ -20,7 +22,7 @@ describe('Subscriber', function(){
status: 200
}));
});
getPreferences('123').then((result)=>{
getPreferences(subscriberId).then((result)=>{
assert.property(result, 'block_in_mode');
assert.isFalse(result.block_in_mode);
assert.property(result, 'clir');
@ -40,7 +42,7 @@ describe('Subscriber', function(){
status: 403
}));
});
getPreferences('123').then(()=>{
getPreferences(subscriberId).then(()=>{
done(new Error('Test failed'));
}).catch((err)=>{
assert.equal(err.status, 403);

@ -0,0 +1,44 @@
'use strict';
import ConversationsModule from '../../src/store/conversations';
import { assert } from 'chai';
describe('Conversations', function(){
it('should load conversations', function(){
let state = {
conversations: [
]
};
let data = [
{
"_links": {
},
"call_type": "cfu",
"caller": "43993010",
"type": "call"
},
{
"_links": {
},
"caller": "43993011",
"type": "fax"
}
];
ConversationsModule.mutations.loadConversations(state, data);
assert.deepEqual(state.conversations, [
{
"call_type": "cfu",
"caller": "43993010",
"type": "call forward"
},
{
"caller": "43993011",
"type": "fax"
}
]);
});
});
Loading…
Cancel
Save