TT#96350 CSC: As Customer, I want to set my own Fax2Mail/Sendfax settings

AC:
Can see a separate main menu item "Fax Settings"
Can click the separate main menu item and land on a page "Fax Settings" (route=/user/fax-settings)
Can enable/disable the feature
Can change the name in fax header for outgoing fax
Can add a new Destination (email, file type, outgoing, incoming, reports)
Can edit a Destination (email, file type, outgoing, incoming, reports)
Can remove a Destination
Can toggle T38 (Fax over Internet)
Can toggle ECM (Error Correction Mode)

In addition there were changed "CscInputSavable" and "CscListMenuItem" components to provide better "disable state" possibility.

Change-Id: I777dd718a6e676acd72f45c175296ce767449469
pull/4/head
Sergii Leonenko 5 years ago
parent efd518f95c
commit e86d06d806

@ -0,0 +1,34 @@
import _ from 'lodash'
import {
get,
patchReplaceFull
} from './common'
import { i18n } from 'src/boot/i18n'
export async function getFaxServerSettings (subscriberId) {
const result = await get({
path: `api/faxserversettings/${subscriberId}`
})
const settings = _.clone(result)
delete settings._links
return settings
}
export async function setFaxServerField (options) {
if (!['name', 'active', 'ecm', 't38', 'destinations'].includes(options.field)) {
throw Error(`setFaxServerField: unknown field name ${options.field}`)
}
if (options.field === 'destinations') {
// searching for duplicates
const destinationsIds = options.value.map(d => d.destination)
if ((new Set(destinationsIds)).size !== destinationsIds.length) {
throw Error(i18n.t('faxSettings.destinationEmailExists'))
}
}
return patchReplaceFull({
path: `api/faxserversettings/${options.subscriberId}`,
fieldPath: options.field,
value: options.value
})
}

@ -6,6 +6,7 @@ import {
getList, getList,
patchReplace patchReplace
} from './common' } from './common'
import { getFaxServerSettings } from 'src/api/fax'
export function login (username, password) { export function login (username, password) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -36,9 +37,9 @@ export function getUserData (id) {
return Promise.all([ return Promise.all([
getSubscriberById(id), getSubscriberById(id),
getCapabilities(id), getCapabilities(id),
getFaxServerSettingsById(id) getFaxServerSettings(id)
]).then((results) => { ]).then((results) => {
results[1].faxactive = results[2] results[1].faxactive = results[2].active
resolve({ resolve({
subscriber: results[0], subscriber: results[0],
capabilities: results[1] capabilities: results[1]
@ -126,15 +127,3 @@ export function getNumbers () {
}) })
}) })
} }
export function getFaxServerSettingsById (id) {
return new Promise((resolve, reject) => {
get({
path: 'api/faxserversettings/' + id
}).then((body) => {
resolve(body.active)
}).catch((err) => {
reject(err)
})
})
}

@ -2,6 +2,7 @@
<q-item <q-item
v-close-popup v-close-popup
clickable clickable
v-bind="$attrs"
@click="click" @click="click"
> >
<q-item-section <q-item-section

@ -140,6 +140,12 @@ export default {
label: this.$t('navigation.voicebox.title'), label: this.$t('navigation.voicebox.title'),
visible: true visible: true
}, },
{
to: '/user/fax-settings',
icon: 'fas fa-fax',
label: this.$t('navigation.faxSettings.title'),
visible: true
},
{ {
icon: 'miscellaneous_services', icon: 'miscellaneous_services',
label: this.$t('navigation.pbxConfiguration.title'), label: this.$t('navigation.pbxConfiguration.title'),

@ -53,6 +53,9 @@ export default {
} }
}, },
methods: { methods: {
show () {
this.open()
},
open () { open () {
this.$refs.dialogComp.open() this.$refs.dialogComp.open()
}, },
@ -62,6 +65,7 @@ export default {
remove () { remove () {
this.close() this.close()
this.$emit('remove') this.$emit('remove')
this.$emit('ok')
}, },
cancel () { cancel () {
this.$emit('cancel') this.$emit('cancel')

@ -2,7 +2,7 @@
<q-input <q-input
v-bind="$attrs" v-bind="$attrs"
:value="value" :value="value"
:disable="$attrs.loading" :disable="disable === true || $attrs.loading"
v-on="$listeners" v-on="$listeners"
@input="$emit('input', $event)" @input="$emit('input', $event)"
@keyup.enter="$emit('save', $event)" @keyup.enter="$emit('save', $event)"
@ -69,6 +69,10 @@ export default {
value: { value: {
type: String, type: String,
default: '' default: ''
},
disable: {
type: Boolean,
default: null
} }
}, },
data () { data () {

@ -0,0 +1,110 @@
<template>
<csc-list-item
icon="email"
:odd="odd"
:expanded="expanded"
@toggle="toggle"
>
<template
slot="title"
>
<csc-list-item-title>
{{ $t('faxSettings.destinationItemTitle', {destination: destination.destination, filetype: destination.filetype}) }}
</csc-list-item-title>
<q-slide-transition>
<csc-list-item-subtitle
v-if="!expanded"
>
<q-icon
size="16px"
:name="destination.incoming ? 'call_received' : ' '"
/>
<q-icon
size="16px"
:name="destination.outgoing ? 'call_made' : ' '"
/>
<q-icon
size="16px"
:name="destination.status ? 'fas fa-file-alt' : ' '"
/>
</csc-list-item-subtitle>
</q-slide-transition>
</template>
<template
slot="menu"
>
<csc-list-menu-item
:disable="loading"
icon="delete"
icon-color="negative"
@click="deleteDestination"
>
{{ $t('buttons.remove') }}
</csc-list-menu-item>
</template>
<template slot="body">
<csc-fax2-mail-destination-form
:is-add-new-mode="false"
:initial-data="destination"
:loading="loading"
@update-property="updateProperty"
/>
</template>
</csc-list-item>
</template>
<script>
import CscListItem from 'components/CscListItem'
import CscListItemTitle from 'components/CscListItemTitle'
import CscListMenuItem from 'components/CscListMenuItem'
import CscListItemSubtitle from 'components/CscListItemSubtitle'
import CscFax2MailDestinationForm from 'components/pages/FaxSettings/CscFax2MailDestinationForm'
export default {
name: 'CscFax2MailDestination',
components: {
CscFax2MailDestinationForm,
CscListItemSubtitle,
CscListMenuItem,
CscListItemTitle,
CscListItem
},
props: {
destination: {
type: Object,
required: true
},
odd: {
type: Boolean,
default: false
},
expanded: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
},
methods: {
deleteDestination () {
if (this.$refs.listItem) {
this.$refs.listItem.closePopoverMenu()
}
this.$emit('remove')
},
toggle () {
if (this.expanded) {
this.$emit('collapse')
} else {
this.$emit('expand')
}
},
updateProperty () {
this.$emit('update-property', ...arguments)
}
}
}
</script>

@ -0,0 +1,169 @@
<template>
<div>
<div class="row">
<div
class="col-xs-12 col-md-6"
>
<csc-input-saveable
v-model="data.destination"
icon="email"
:label="$t('faxSettings.destinationEmail')"
:disable="disabled"
:readonly="loading"
:error="$v.data.destination.$error"
:error-message="destinationErrorMessage"
:value-changed="!isAddNewMode && data.destination !== initialData.destination"
@input="$v.data.destination.$touch"
@keypress.space.prevent
@keydown.space.prevent
@keyup.space.prevent
@undo="data.destination = initialData.destination"
@save="updatePropertyData('destination')"
/>
<q-select
v-model="data.filetype"
dense
emit-value
map-options
:disable="loading"
:readonly="loading"
:label="$t('faxSettings.fileType')"
:options="fileTypeOptions"
@input="updatePropertyData('filetype')"
/>
</div>
<div
class="col-xs-12 col-md-6"
>
<q-toggle
v-model="data.incoming"
:label="$t('faxSettings.deliverIncomingFaxes')"
:disable="loading"
@input="updatePropertyData('incoming')"
/>
<q-toggle
v-model="data.outgoing"
:label="$t('faxSettings.deliverOutgoingFaxes')"
:disable="loading"
@input="updatePropertyData('outgoing')"
/>
<q-toggle
v-model="data.status"
:label="$t('faxSettings.receiveReports')"
:disable="loading"
@input="updatePropertyData('status')"
/>
</div>
</div>
<div
v-if="isAddNewMode"
class="row justify-center"
>
<q-btn
flat
color="default"
icon="clear"
:disable="loading"
:label="$t('buttons.cancel')"
@click="cancel()"
/>
<q-btn
flat
color="primary"
icon="person"
:loading="loading"
:disable="$v.data.$invalid || loading"
:label="$t('faxSettings.createDestination')"
@click="save()"
/>
</div>
</div>
</template>
<script>
import { email, required } from 'vuelidate/lib/validators'
import CscInputSaveable from 'components/form/CscInputSaveable'
export default {
name: 'CscFax2MailDestinationForm',
components: {
CscInputSaveable
},
props: {
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
initialData: {
type: Object,
default: () => ({
destination: '',
filetype: 'TIFF',
incoming: true,
outgoing: true,
status: true
})
},
isAddNewMode: {
type: Boolean,
default: false
}
},
data () {
return {
data: this.getDefaults()
}
},
validations: {
data: {
destination: {
required,
email
}
}
},
computed: {
destinationErrorMessage () {
if (!this.$v.data.destination.required) {
return this.$t('validationErrors.fieldRequired', {
field: this.$t('faxSettings.destinationEmail')
})
} else if (!this.$v.data.destination.email) {
return this.$t('validationErrors.email')
} else {
return ''
}
},
fileTypeOptions () {
return ['TIFF', 'PS', 'PDF', 'PDF14']
}
},
methods: {
getDefaults () {
return { ...this.initialData }
},
cancel () {
this.$emit('cancel')
},
save () {
this.$emit('save', {
...this.data
})
},
reset () {
this.data = this.getDefaults()
this.$v.$reset()
},
updatePropertyData (propertyName) {
this.$emit('update-property', {
name: propertyName,
value: this.data[propertyName]
})
}
}
}
</script>

@ -121,6 +121,10 @@
"title": "Voicebox", "title": "Voicebox",
"subTitle": "Set your voicebox settings" "subTitle": "Set your voicebox settings"
}, },
"faxSettings": {
"title": "Fax Settings",
"subTitle": "Set your fax settings"
},
"conference": { "conference": {
"title": "Join conference" "title": "Join conference"
}, },
@ -736,5 +740,23 @@
"deleteVoicemailButton": "Remove Voicemail", "deleteVoicemailButton": "Remove Voicemail",
"deleteVoicemailTitle": "Remove Voicemail", "deleteVoicemailTitle": "Remove Voicemail",
"deleteVoicemailText": "You are about to remove this Voicemail" "deleteVoicemailText": "You are about to remove this Voicemail"
} },
"faxSettings": {
"active": "Active",
"sendfaxHeaderName": "Name in Fax Header for Sendfax",
"T38": "T38",
"ECM": "ECM",
"noDestinationsCreatedYet": "No destinations created yet",
"addDestination": "Add destination",
"createDestination": "Create destination",
"destinationEmail": "Destination Email",
"fileType": "File Type",
"deliverIncomingFaxes": "Deliver Incoming Faxes",
"deliverOutgoingFaxes": "Deliver Outgoing Faxes",
"receiveReports": "Receive Reports",
"deleteDestinationTitle": "Remove Destination",
"deleteDestinationText": "You are about to remove destination {destination}",
"destinationEmailExists": "The Destination Email is already used",
"destinationItemTitle": "<{destination}> as {filetype}"
}
} }

@ -0,0 +1,276 @@
<template>
<csc-page
class="q-pa-lg"
>
<q-list
class="col col-xs-12 col-md-6"
dense
>
<q-item>
<q-item-section>
<q-toggle
v-model="faxToMailSettings.active"
:label="$t('faxSettings.active')"
:disable="!dataLoaded"
@input="setChangedData('active', !faxServerSettings.active)"
/>
</q-item-section>
<q-item-section
side
>
<csc-spinner
v-if="loadingFaxServerSettings"
class="self-center"
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<csc-input-saveable
v-model.trim="faxToMailSettings.name"
:label="$t('faxSettings.sendfaxHeaderName')"
:disable="!dataLoaded"
:loading="loadingFaxServerSettings"
:value-changed="nameChanged"
@save="setChangedData('name', faxToMailSettings.name)"
@undo="restoreName"
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-toggle
v-model="faxToMailSettings.t38"
:label="$t('faxSettings.T38')"
:disable="!dataLoaded"
@input="setChangedData('t38', !faxServerSettings.t38)"
/>
</q-item-section>
<q-item-section
side
>
<csc-spinner
v-if="loadingFaxServerSettings"
class="self-center"
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-toggle
v-model="faxToMailSettings.ecm"
:label="$t('faxSettings.ECM')"
:disable="!dataLoaded"
@input="setChangedData('ecm', !faxServerSettings.ecm)"
/>
</q-item-section>
<q-item-section
side
>
<csc-spinner
v-if="loadingFaxServerSettings"
class="self-center"
/>
</q-item-section>
</q-item>
<q-item class="row">
<div class="col">
<span class="text-h6">Destinations:</span>
</div>
<div class="col text-center">
<csc-spinner
v-if="loadingFaxServerSettings"
/>
</div>
<div class="col text-right">
<q-btn
flat
color="primary"
icon="add"
:disable="!dataLoaded || showAddNewDestination"
@click="openAddNewDestination"
>
{{ $t('faxSettings.addDestination') }}
</q-btn>
</div>
</q-item>
</q-list>
<q-separator />
<div
class="row justify-center q-mb-lg"
>
<q-list
class="col-xs-12"
>
<q-item
v-if="showAddNewDestination"
class="row justify-center"
>
<csc-fax2-mail-destination-form
v-if="showAddNewDestination"
ref="addNewDestination"
:loading="loadingFaxServerSettings"
:is-add-new-mode="true"
@save="addNewDestination"
@cancel="closeAddNewDestination"
/>
</q-item>
<q-item
v-if="!hasDestinations"
class="row justify-center"
>
{{ $t('faxSettings.noDestinationsCreatedYet') }}
</q-item>
<csc-fax2-mail-destination
v-for="(destinationItem, index) in faxToMailSettings.destinations"
:key="destinationItem.destination"
:odd="(index % 2) === 0"
:expanded="expandedDestinationId === destinationItem.destination"
:destination="destinationItem"
:loading="loadingFaxServerSettings"
@collapse="expandedDestinationId = null"
@expand="expandedDestinationId = destinationItem.destination"
@remove="openDeleteDestinationDialog(destinationItem.destination)"
@update-property="updateDestinationItemProperty(destinationItem.destination, ...arguments)"
/>
</q-list>
</div>
</csc-page>
</template>
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import CscInputSaveable from 'components/form/CscInputSaveable'
import CscPage from 'components/CscPage'
import CscSpinner from 'components/CscSpinner'
import { mapWaitingActions, mapWaitingGetters } from 'vue-wait'
import CscFax2MailDestinationForm from 'components/pages/FaxSettings/CscFax2MailDestinationForm'
import CscFax2MailDestination from 'components/pages/FaxSettings/CscFax2MailDestination'
import CscRemoveDialog from 'components/CscRemoveDialog'
import { showGlobalError } from 'src/helpers/ui'
export default {
name: 'CscPageFaxSettings',
components: {
CscFax2MailDestination,
CscFax2MailDestinationForm,
CscSpinner,
CscPage,
CscInputSaveable
},
data () {
return {
faxToMailSettings: {},
showAddNewDestination: false,
expandedDestinationId: null
}
},
computed: {
...mapState('fax', [
'faxServerSettings',
'faxServerSettingsInitialized'
]),
...mapWaitingGetters({
loadingFaxServerSettings: 'loading faxServerSettings'
}),
dataLoaded () {
return this.faxServerSettingsInitialized && !this.loadingFaxServerSettings
},
hasDestinations () {
return this.faxToMailSettings?.destinations?.length
},
nameChanged () {
return this.faxToMailSettings.name !== this.faxServerSettings.name
}
},
mounted () {
this.loadFaxServerSettings()
},
methods: {
...mapWaitingActions('fax', {
loadFaxSettingsAction: 'loading faxServerSettings',
fieldUpdateAction: 'loading faxServerSettings'
}),
async loadFaxServerSettings () {
try {
await this.loadFaxSettingsAction()
this.updateDataFromStore()
} catch (err) {
showGlobalError(err?.message)
}
},
updateDataFromStore () {
this.faxToMailSettings = _.cloneDeep(this.faxServerSettings)
},
async setChangedData (field, value) {
try {
await this.fieldUpdateAction({ field, value })
this.updateDataFromStore()
} catch (err) {
showGlobalError(err?.message)
}
},
restoreName () {
this.faxToMailSettings.name = this.faxServerSettings.name
},
async updateDestinations (destinationItems, beforeUpdateUI = () => {}) {
try {
await this.fieldUpdateAction({
field: 'destinations',
value: destinationItems
})
beforeUpdateUI()
this.updateDataFromStore()
} catch (err) {
showGlobalError(err?.message)
}
},
openAddNewDestination () {
this.showAddNewDestination = true
},
closeAddNewDestination () {
this.showAddNewDestination = false
this.$refs.addNewDestination.reset()
},
addNewDestination (destination) {
const destinationItems = [...this.faxToMailSettings.destinations, destination]
this.updateDestinations(destinationItems, () => {
this.closeAddNewDestination()
})
},
deleteDestination (destinationId) {
const destinationItems = this.faxToMailSettings.destinations.filter(d => d.destination !== destinationId)
this.fieldUpdateAction({
field: 'destinations',
value: destinationItems
}).then(() => {
if (this.expandedDestinationId === destinationId) {
this.expandedDestinationId = null
}
this.updateDataFromStore()
})
},
openDeleteDestinationDialog (destinationId) {
this.$q.dialog({
component: CscRemoveDialog,
parent: this,
title: this.$t('faxSettings.deleteDestinationTitle'),
message: this.$t('faxSettings.deleteDestinationText', { destination: destinationId })
}).onOk(() => {
this.deleteDestination(destinationId)
})
},
updateDestinationItemProperty (destinationId, data) {
const destinationItems = _.cloneDeep(this.faxToMailSettings.destinations)
const destinationItemIndex = destinationItems.findIndex(d => d.destination === destinationId)
if (destinationItemIndex >= 0) {
destinationItems[destinationItemIndex][data.name] = data.value
}
this.updateDestinations(destinationItems)
}
}
}
</script>

@ -23,6 +23,7 @@ import CscPagePbxSoundSets from 'src/pages/CscPagePbxSoundSets'
import CscPagePbxMsConfigs from 'src/pages/CscPagePbxMsConfigs' import CscPagePbxMsConfigs from 'src/pages/CscPagePbxMsConfigs'
import CscPagePbxSettings from 'src/pages/CscPagePbxSettings' import CscPagePbxSettings from 'src/pages/CscPagePbxSettings'
import CscPageVoicebox from 'src/pages/CscPageVoicebox' import CscPageVoicebox from 'src/pages/CscPageVoicebox'
import CscPageFaxSettings from 'src/pages/CscPageFaxSettings'
import CscPageUserSettings from 'src/pages/CscPageUserSettings' import CscPageUserSettings from 'src/pages/CscPageUserSettings'
import CscPageError404 from 'src/pages/CscPageError404' import CscPageError404 from 'src/pages/CscPageError404'
import CscRecoverPassword from 'src/pages/CscRecoverPassword' import CscRecoverPassword from 'src/pages/CscRecoverPassword'
@ -179,6 +180,14 @@ export default function routes (app) {
subtitle: i18n.t('navigation.voicebox.subTitle') subtitle: i18n.t('navigation.voicebox.subTitle')
} }
}, },
{
path: 'fax-settings',
component: CscPageFaxSettings,
meta: {
title: i18n.t('navigation.faxSettings.title'),
subtitle: i18n.t('navigation.faxSettings.subTitle')
}
},
{ {
path: 'settings', path: 'settings',
component: CscPageUserSettings, component: CscPageUserSettings,

@ -0,0 +1,45 @@
import _ from 'lodash'
import {
getFaxServerSettings,
setFaxServerField
} from '../api/fax'
export default {
namespaced: true,
state: {
faxServerSettingsInitialized: false,
faxServerSettings: {}
},
getters: {
subscriberId (state, getters, rootState, rootGetters) {
return parseInt(rootGetters['user/getSubscriberId'])
}
},
mutations: {
settingsSucceeded (state, res) {
if (_.has(res, 'faxServerSettings')) {
state.faxServerSettings = res.faxServerSettings
state.faxServerSettingsInitialized = true
}
}
},
actions: {
async loadFaxSettingsAction (context) {
const faxServerSettings = await getFaxServerSettings(context.getters.subscriberId)
context.commit('settingsSucceeded', {
faxServerSettings
})
},
async fieldUpdateAction (context, options) {
const faxServerSettings = await setFaxServerField({
subscriberId: context.getters.subscriberId,
field: options.field,
value: options.value
})
context.commit('settingsSucceeded', {
faxServerSettings
})
context.commit('user/updateFaxActiveCapabilityState', faxServerSettings.active, { root: true })
}
}
}

@ -23,6 +23,7 @@ import ReminderModule from './reminder'
import SpeedDialModule from './speed-dial' import SpeedDialModule from './speed-dial'
import UserModule from './user' import UserModule from './user'
import CommunicationModule from './communication' import CommunicationModule from './communication'
import FaxModule from './fax'
import VoiceboxModule from './voicebox' import VoiceboxModule from './voicebox'
import ConferenceModule from './conference' import ConferenceModule from './conference'
@ -60,6 +61,7 @@ export default function (/* { ssrContext } */) {
speedDial: SpeedDialModule, speedDial: SpeedDialModule,
user: UserModule, user: UserModule,
communication: CommunicationModule, communication: CommunicationModule,
fax: FaxModule,
voicebox: VoiceboxModule, voicebox: VoiceboxModule,
conference: ConferenceModule, conference: ConferenceModule,
pbx: PbxModule, pbx: PbxModule,

@ -249,6 +249,9 @@ export default {
}, },
newPasswordRequesting (state, isRequesting) { newPasswordRequesting (state, isRequesting) {
state.newPasswordRequesting = isRequesting state.newPasswordRequesting = isRequesting
},
updateFaxActiveCapabilityState (state, value) {
state.capabilities.faxactive = value
} }
}, },
actions: { actions: {

Loading…
Cancel
Save