TT#80753 Use a smaller preview version of each PBX device image in order to save bandwidth and improve render performance

Change-Id: I84b03ad7146af767ae9c6bb0e336fca2ce8f5874
mr9.1.1
Hans-Peter Herzog 5 years ago
parent b038c533d8
commit 075452c698

4
env/run_csc_ui vendored

@ -38,13 +38,13 @@ echo "JFYI, important components versions:"
echo -n "node --version : " && node --version echo -n "node --version : " && node --version
echo -n "yarn --version : " && yarnpkg --version echo -n "yarn --version : " && yarnpkg --version
echo "Configuring Vue.js/Quasar dev environment, running 'npm ci'..." echo "Configuring Vue.js/Quasar dev environment, running 'yarnpkg install'..."
if ! yarnpkg install ; then if ! yarnpkg install ; then
echo "ERROR: cannot install all npm dependencies. Aborting." echo "ERROR: cannot install all npm dependencies. Aborting."
exit 1 exit 1
fi fi
echo "Starting Quasar dev environment, running 'npm run dev'..." echo "Starting Quasar dev environment, running 'yarnpkg run dev'..."
if ! yarnpkg run dev ; then if ! yarnpkg run dev ; then
echo "ERROR: cannot run quasar dev environment. Aborting." echo "ERROR: cannot run quasar dev environment. Aborting."
exit 1 exit 1

@ -50,6 +50,7 @@
"eslint-plugin-promise": "^4.0.1", "eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.1.2", "eslint-plugin-vue": "^6.1.2",
"generate-password": "^1.5.1",
"parseuri": "^0.0.6" "parseuri": "^0.0.6"
}, },
"browserslist": [ "browserslist": [

@ -73,27 +73,34 @@ export function getModel (id) {
}) })
} }
export function getModelFrontImage (id) { export async function getModelImage (id, type) {
return new Promise((resolve) => { try {
Vue.http.get('api/pbxdevicemodelimages/' + id, { const res = await Vue.http.get('api/pbxdevicemodelimages/' + id, {
responseType: 'blob', responseType: 'blob',
params: { params: {
type: 'front' type: type
} }
}).then((res) => {
resolve({
id: id,
url: URL.createObjectURL(res.body),
blob: res.body
})
}).catch(() => {
resolve({
id: id,
url: null,
blob: null
})
}) })
}) return {
id: id,
url: URL.createObjectURL(res.body),
blob: res.body
}
} catch (err) {
return {
id: id,
url: null,
blob: null
}
}
}
export async function getModelFrontImage (id) {
return getModelImage(id, 'front')
}
export async function getModelFrontThumbnailImage (id) {
return getModelImage(id, 'front_thumb')
} }
export function getAllSoundSets (options) { export function getAllSoundSets (options) {

@ -162,7 +162,7 @@ export function setDeviceKeys (deviceId, keys) {
}) })
} }
export function loadDeviceModel (modelId) { export async function loadDeviceModel (modelId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Promise.all([ Promise.all([
getModel(modelId), getModel(modelId),

@ -80,16 +80,29 @@ export function getSeatList (options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const page = _.get(options, 'page', 1) const page = _.get(options, 'page', 1)
const displayName = _.get(options, 'display_name', null) const displayName = _.get(options, 'display_name', null)
const pbxExtension = _.get(options, 'pbx_extension', null)
const primaryNumber = _.get(options, 'primary_number', null)
const aliasNumber = _.get(options, 'alias_number', null)
const params = { const params = {
page: page, page: page,
order_by: PBX_CONFIG_ORDER_BY, order_by: PBX_CONFIG_ORDER_BY,
order_by_direction: PBX_CONFIG_ORDER_DIRECTION order_by_direction: PBX_CONFIG_ORDER_DIRECTION
} }
if (displayName) {
params.display_name = displayName
}
if (pbxExtension) {
params.pbx_extension = pbxExtension
}
if (primaryNumber) {
params.primary_number = primaryNumber
}
if (aliasNumber) {
params.alias_number = aliasNumber
}
Promise.all([ Promise.all([
getSeats({ getSeats({
params: displayName ? _.merge({ params: params
display_name: displayName
}, params) : params
}), }),
getGroupsOnly({ getGroupsOnly({
all: true all: true

@ -39,12 +39,11 @@
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<form> <form>
<q-input <csc-input
v-model="username" v-model="username"
class="q-mb-sm" class="q-mb-sm"
type="text" type="text"
max-length="128" max-length="128"
flat
:label="$t('pages.login.username')" :label="$t('pages.login.username')"
:disable="loginRequesting" :disable="loginRequesting"
autofocus autofocus
@ -58,26 +57,34 @@
name="person" name="person"
/> />
</template> </template>
</q-input> </csc-input>
<q-input <csc-input-password
v-model="password" v-model="password"
class="q-mb-sm"
type="password"
max-length="32" max-length="32"
flat
:label="$t('pages.login.password')" :label="$t('pages.login.password')"
:disable="loginRequesting" :disable="loginRequesting"
clearable clearable
@keyup.enter="login()" @keypress.enter="login()"
> />
<template <!-- <q-input-->
slot="prepend" <!-- v-model="password"-->
> <!-- class="q-mb-sm"-->
<q-icon <!-- type="password"-->
name="lock" <!-- max-length="32"-->
/> <!-- flat-->
</template> <!-- :label="$t('pages.login.password')"-->
</q-input> <!-- :disable="loginRequesting"-->
<!-- clearable-->
<!-- @keyup.enter="login()"-->
<!-- >-->
<!-- <template-->
<!-- slot="prepend"-->
<!-- >-->
<!-- <q-icon-->
<!-- name="lock"-->
<!-- />-->
<!-- </template>-->
<!-- </q-input>-->
</form> </form>
</q-card-section> </q-card-section>
<q-card-actions <q-card-actions
@ -120,9 +127,13 @@ import {
} from '../i18n' } from '../i18n'
import CscLanguageMenu from './CscLanguageMenu' import CscLanguageMenu from './CscLanguageMenu'
import CscSpinner from 'components/CscSpinner' import CscSpinner from 'components/CscSpinner'
import CscInputPassword from 'components/form/CscInputPassword'
import CscInput from 'components/form/CscInput'
export default { export default {
name: 'Login', name: 'Login',
components: { components: {
CscInput,
CscInputPassword,
CscSpinner, CscSpinner,
CscLanguageMenu CscLanguageMenu
}, },

@ -0,0 +1,65 @@
<template>
<csc-page
:style="pageStyle"
>
<q-page-sticky
ref="pageSticky"
class="bg-secondary q-pt-md"
style="z-index: 10"
expand
position="top"
>
<slot
name="header"
/>
<q-separator />
<div
class="col-12"
>
<slot
name="toolbar"
/>
</div>
<q-resize-observer
@resize="computeTopMargin"
/>
</q-page-sticky>
<slot />
</csc-page>
</template>
<script>
import CscPage from 'components/CscPage'
export default {
name: 'CscPageSticky',
components: {
CscPage
},
data () {
return {
topMargin: 0
}
},
computed: {
pageStyle () {
return {
paddingTop: this.topMargin + 'px'
}
}
},
mounted () {
this.computeTopMargin()
},
methods: {
input ($event) {
this.$emit('input', $event)
this.computeTopMargin()
this.$nextTick(() => {
this.computeTopMargin()
})
},
computeTopMargin () {
this.topMargin = this.$refs.pageSticky.$el.offsetHeight + 36
}
}
}
</script>

@ -32,6 +32,7 @@
<script> <script>
import CscPage from 'components/CscPage' import CscPage from 'components/CscPage'
export default { export default {
name: 'QPageStickyTabs',
components: { components: {
CscPage CscPage
}, },

@ -2,7 +2,7 @@
<div <div
class="csc-form" class="csc-form"
> >
<q-input <csc-input-password
ref="passwordInput" ref="passwordInput"
v-model.trim="password" v-model.trim="password"
clearable clearable
@ -13,15 +13,7 @@
:error="$v.password.$error" :error="$v.password.$error"
:error-message="errorMessagePass" :error-message="errorMessagePass"
@blur="$v.password.$touch()" @blur="$v.password.$touch()"
> />
<template
v-slot:prepend
>
<q-icon
name="lock"
/>
</template>
</q-input>
<password-strength-meter <password-strength-meter
v-model="passwordScored" v-model="passwordScored"
class="full-width" class="full-width"
@ -29,7 +21,7 @@
:strength-meter-only="true" :strength-meter-only="true"
@score="strengthMeterScoreUpdate" @score="strengthMeterScoreUpdate"
/> />
<q-input <csc-input-password
ref="passwordRetypeInput" ref="passwordRetypeInput"
v-model.trim="passwordRetype" v-model.trim="passwordRetype"
clearable clearable
@ -40,15 +32,7 @@
:error="$v.passwordRetype.$error" :error="$v.passwordRetype.$error"
:error-message="errorMessagePassRetype" :error-message="errorMessagePassRetype"
@blur="$v.passwordRetype.$touch();onRetypeBlur()" @blur="$v.passwordRetype.$touch();onRetypeBlur()"
> />
<template
v-slot:prepend
>
<q-icon
name="lock"
/>
</template>
</q-input>
</div> </div>
</template> </template>
@ -57,9 +41,11 @@ import PasswordStrengthMeter from 'vue-password-strength-meter'
import { import {
required required
} from 'vuelidate/lib/validators' } from 'vuelidate/lib/validators'
import CscInputPassword from 'components/form/CscInputPassword'
export default { export default {
name: 'CscChangePasswordForm', name: 'CscChangePasswordForm',
components: { components: {
CscInputPassword,
PasswordStrengthMeter PasswordStrengthMeter
}, },
props: { props: {

@ -1,7 +1,7 @@
<template> <template>
<q-input <q-input
ref="input"
:value="value" :value="value"
:clearable="false"
v-bind="$attrs" v-bind="$attrs"
@input="$emit('input', $event)" @input="$emit('input', $event)"
v-on="$listeners" v-on="$listeners"
@ -24,16 +24,18 @@
<template <template
v-slot:append v-slot:append
> >
<slot
name="append"
/>
<q-btn <q-btn
v-if="value !== ''" v-if="$attrs.clearable !== undefined && value !== ''"
icon="clear" icon="clear"
color="white" color="white"
flat flat
dense dense
@click="$emit('clear', $event)" tabindex="-1"
/> :disable="$attrs.disable"
<slot @click="clear"
name="append"
/> />
</template> </template>
</q-input> </q-input>
@ -42,6 +44,7 @@
<script> <script>
import CscSpinner from 'components/CscSpinner' import CscSpinner from 'components/CscSpinner'
export default { export default {
name: 'CscInput',
components: { components: {
CscSpinner CscSpinner
}, },
@ -52,9 +55,17 @@ export default {
} }
}, },
date () { date () {
return {} return {
}
}, },
mounted () { mounted () {
},
methods: {
clear () {
this.$emit('input', '')
this.$emit('clear')
}
} }
} }
</script> </script>

@ -0,0 +1,94 @@
<template>
<csc-input
ref="input"
v-bind="$attrs"
:type="inputType"
:value="value"
@input="$emit('input', $event)"
v-on="$listeners"
>
<template
slot="prepend"
>
<q-icon
name="lock"
/>
</template>
<template
v-slot:append
>
<q-btn
v-if="value !== ''"
:icon="icon"
:disable="$attrs.disable"
tabindex="-1"
color="primary"
flat
dense
@click.stop="visible=!visible"
/>
<q-btn
v-if="generate"
icon="casino"
:disable="$attrs.disable"
tabindex="-1"
color="primary"
flat
dense
@click.stop="generatePassword"
/>
</template>
</csc-input>
</template>
<script>
import CscInput from 'components/form/CscInput'
import PasswordGenerator from 'generate-password'
export default {
name: 'CscInputPassword',
components: { CscInput },
props: {
value: {
type: String,
default: undefined
},
generate: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false
}
},
computed: {
inputType () {
if (this.visible) {
return 'text'
} else {
return 'password'
}
},
icon () {
if (!this.visible) {
return 'visibility_off'
} else {
return 'visibility'
}
}
},
methods: {
generatePassword () {
const pass = PasswordGenerator.generate({
length: 10,
numbers: true
})
this.$emit('input', pass)
this.$emit('generated', pass)
},
clear () {
this.$refs.input.clear()
}
}
}
</script>

@ -0,0 +1,144 @@
<template>
<div
class="csc-input-password-retype"
>
<csc-input-password
v-model="password"
v-bind="$attrs"
generate
clearable
:label="$t('pbxConfig.typePassword')"
@input="inputPassword"
@generated="passwordGenerated"
@clear="$refs.passwordRetype.clear()"
/>
<password-strength-meter
v-show="false"
v-model="password"
:strength-meter-only="true"
@score="strengthMeterScoreUpdate"
/>
<q-linear-progress
v-model="passwordScoreMappedValue"
:color="passwordScoreColor"
size="8px"
/>
<csc-input-password
ref="passwordRetype"
v-model="passwordRetype"
v-bind="$attrs"
:label="$t('pbxConfig.retypePassword')"
:error="$v.passwordRetype.$error"
:error-message="errorMessagePasswordRetype"
clearable
:disable="passwordScore < 2 || $attrs.disable"
@clear="$v.passwordRetype.$reset"
@blur="blur"
@input="inputRetypePassword"
/>
</div>
</template>
<script>
import {
sameAs,
required
} from 'vuelidate/lib/validators'
import CscInputPassword from 'components/form/CscInputPassword'
import PasswordStrengthMeter from 'vue-password-strength-meter'
export default {
name: 'CscInputPasswordRetype',
components: {
CscInputPassword,
PasswordStrengthMeter
},
validations: {
password: {
required
},
passwordRetype: {
sameAsPassword: sameAs('password')
}
},
props: {
value: {
type: Object,
default () {
return {
password: '',
passwordRetype: ''
}
}
}
},
data () {
return {
password: this.value.password,
passwordRetype: this.value.passwordRetype,
passwordScore: null
}
},
computed: {
errorMessagePasswordRetype () {
if (!this.$v.passwordRetype.sameAsPassword) {
return this.$t('pbxConfig.errorPasswordNotEqual')
} else {
return ''
}
},
passwordScoreMappedValue () {
if (this.passwordScore === null || this.passwordScore === undefined) {
return 0
}
return (this.passwordScore + 1) / 5
},
passwordScoreColor () {
if (this.passwordScore < 2) {
return 'negative'
} else if (this.passwordScore === 2) {
return 'warning'
} else {
return 'primary'
}
}
},
watch: {
value (value) {
this.password = value.password
this.passwordRetype = value.passwordRetype
}
},
methods: {
strengthMeterScoreUpdate (score) {
this.passwordScore = score
this.$emit('score', score)
},
inputPassword () {
this.$emit('input', {
password: this.password,
passwordRetype: this.passwordRetype
})
},
inputRetypePassword () {
this.$v.passwordRetype.$reset()
this.inputPassword()
},
passwordGenerated (password) {
this.$emit('input', {
password: password,
passwordRetype: password
})
},
blur () {
this.$v.passwordRetype.$touch()
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.csc-input-password-retype
.Password__strength-meter
margin 0
margin-top 16px !important
margin-bottom 16px !important
</style>

@ -1,6 +1,6 @@
<template> <template>
<csc-list-item <csc-list-item
icon="fa-fax" icon="fas fa-fax"
:image="imageUrl" :image="imageUrl"
:odd="odd" :odd="odd"
:expanded="expanded" :expanded="expanded"
@ -102,23 +102,6 @@
/> />
</template> </template>
</csc-pbx-model-select> </csc-pbx-model-select>
<!-- <q-field-->
<!-- -->
<!-- >-->
<!-- <csc-fade>-->
<!-- <csc-form-save-button-->
<!-- v-if="hasProfileChanged"-->
<!-- @click="save"-->
<!-- />-->
<!-- </csc-fade>-->
<!-- <csc-fade>-->
<!-- <csc-form-reset-button-->
<!-- v-if="hasProfileChanged"-->
<!-- @click="resetProfile"-->
<!-- />-->
<!-- </csc-fade>-->
<!-- </q-field>-->
<csc-pbx-device-config <csc-pbx-device-config
v-if="modelImage" v-if="modelImage"
:device="device" :device="device"
@ -254,6 +237,9 @@ export default {
if (expanded) { if (expanded) {
this.$emit('expanded') this.$emit('expanded')
} }
},
profile () {
this.$emit('load-model')
} }
}, },
mounted () { mounted () {

@ -37,7 +37,7 @@
:model-image-map="deviceModelImageMap" :model-image-map="deviceModelImageMap"
@cancel="disableDeviceAddForm" @cancel="disableDeviceAddForm"
@submit="createDevice" @submit="createDevice"
@model-select-opened="loadDeviceModels" @model-select-opened="loadDeviceModels('front_thumb')"
/> />
</div> </div>
</q-slide-transition> </q-slide-transition>
@ -59,7 +59,7 @@
@reset-profile="resetProfileFilter" @reset-profile="resetProfileFilter"
@close-filters="closeFilters" @close-filters="closeFilters"
@reset-filters="resetFilters" @reset-filters="resetFilters"
@model-select-opened="loadDeviceModels" @model-select-opened="loadDeviceModels('front_thumb')"
/> />
</q-slide-transition> </q-slide-transition>
<div <div
@ -98,7 +98,10 @@
:subscriber-map="subscriberMap" :subscriber-map="subscriberMap"
:subscribers-loading="isSubscribersRequesting" :subscribers-loading="isSubscribersRequesting"
:subscriber-options="getSubscriberOptions" :subscriber-options="getSubscriberOptions"
@load-model="loadDeviceModel(deviceProfileMap[device.profile_id].device_id)" @load-model="loadDeviceModel({
type: 'all',
deviceId: deviceProfileMap[device.profile_id].device_id
})"
@expand="expandDevice(device.id)" @expand="expandDevice(device.id)"
@collapse="collapseDevice" @collapse="collapseDevice"
@expanded="deviceExpanded" @expanded="deviceExpanded"
@ -107,7 +110,7 @@
@save-identifier="setDeviceIdentifier" @save-identifier="setDeviceIdentifier"
@save-profile="setDeviceProfile" @save-profile="setDeviceProfile"
@save-keys="setDeviceKeys" @save-keys="setDeviceKeys"
@model-select-opened="loadDeviceModels" @model-select-opened="loadDeviceModels('front_thumb')"
/> />
</csc-fade> </csc-fade>
</csc-list> </csc-list>

@ -36,7 +36,7 @@
v-on="scope.itemEvents" v-on="scope.itemEvents"
> >
<q-item-section <q-item-section
v-if="!modelImageMap[scope.opt.model]" v-if="!deviceModelImageSmallMap[scope.opt.model]"
side side
> >
<q-icon <q-icon
@ -52,7 +52,7 @@
square square
> >
<img <img
:src="modelImageMap[scope.opt.model].url" :src="deviceModelImageSmallMap[scope.opt.model].url"
> >
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
@ -160,6 +160,9 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import {
mapState
} from 'vuex'
export default { export default {
name: 'CscPbxModelSelect', name: 'CscPbxModelSelect',
props: { props: {
@ -198,12 +201,15 @@ export default {
} }
}, },
computed: { computed: {
...mapState('pbx', [
'deviceModelImageSmallMap'
]),
selectedProfileName () { selectedProfileName () {
return _.get(this.selectedProfile, 'name', '') return _.get(this.selectedProfile, 'name', '')
}, },
selectedProfileImageUrl () { selectedProfileImageUrl () {
const deviceModelId = _.get(this.selectedProfile, 'device_id', null) const deviceModelId = _.get(this.selectedProfile, 'device_id', null)
return _.get(this.modelImageMap, deviceModelId + '.url', null) return _.get(this.deviceModelImageSmallMap, deviceModelId + '.url', null)
}, },
options () { options () {
const options = [] const options = []

@ -10,6 +10,7 @@
<q-item-section <q-item-section
side side
top top
no-wrap
> >
<q-icon <q-icon
name="person" name="person"
@ -25,7 +26,12 @@
<q-item-label <q-item-label
caption caption
> >
{{ $t('pbxConfig.extension') }}: {{ seat.pbx_extension }} {{ $t('pbxConfig.webusername') }}: <strong>{{ seat.webusername }}</strong>
</q-item-label>
<q-item-label
caption
>
{{ $t('pbxConfig.extension') }}: <strong>{{ seat.pbx_extension }}</strong>
</q-item-label> </q-item-label>
<q-item-label <q-item-label
caption caption
@ -63,22 +69,35 @@
side side
> >
<csc-more-menu> <csc-more-menu>
<csc-popup-menu-item
icon="vpn_key"
color="primary"
:label="$t('pbxConfig.editPassword')"
@click="showPasswordDialog"
/>
<csc-popup-menu-item-delete <csc-popup-menu-item-delete
@click="deleteSeat" @click="deleteSeat"
/> />
<q-separator />
<q-item
class="no-padding"
>
<q-item-section>
<q-toggle
v-model="changes.clirIntrapbx"
class="q-pa-sm"
:label="$t('pbxConfig.toggleIntraPbx')"
:disable="loading"
@input="changeIntraPbx"
/>
</q-item-section>
</q-item>
</csc-more-menu> </csc-more-menu>
</q-item-section> </q-item-section>
</template> </template>
<div <div
class="q-pa-md" class="q-pa-md"
> >
<q-btn
icon="vpn_key"
flat
color="primary"
:label="$t('pbxConfig.editPassword')"
@click="showPasswordDialog"
/>
<csc-change-password-dialog <csc-change-password-dialog
ref="changePasswordDialog" ref="changePasswordDialog"
:loading="false" :loading="false"
@ -224,9 +243,11 @@ import CscInputButtonSave from 'components/form/CscInputButtonSave'
import CscInputButtonReset from 'components/form/CscInputButtonReset' import CscInputButtonReset from 'components/form/CscInputButtonReset'
import CscMoreMenu from 'components/CscMoreMenu' import CscMoreMenu from 'components/CscMoreMenu'
import CscPopupMenuItemDelete from 'components/CscPopupMenuItemDelete' import CscPopupMenuItemDelete from 'components/CscPopupMenuItemDelete'
import CscPopupMenuItem from 'components/CscPopupMenuItem'
export default { export default {
name: 'CscPbxSeat', name: 'CscPbxSeat',
components: { components: {
CscPopupMenuItem,
CscPopupMenuItemDelete, CscPopupMenuItemDelete,
CscMoreMenu, CscMoreMenu,
CscInputButtonReset, CscInputButtonReset,

@ -1,15 +1,16 @@
<template> <template>
<div> <div>
<div <div
class="row justify-center q-gutter-lg q-mb-md" class="row justify-center q-gutter-x-sm q-pt-sm"
> >
<div <div
class="col col-3" class="col col-3"
> >
<q-input <csc-input
v-model="data.name" v-model="data.name"
clearable clearable
autofocus autofocus
dense
hide-bottom-space hide-bottom-space
:error="$v.data.name.$error" :error="$v.data.name.$error"
:error-message="seatNameErrorMessage" :error-message="seatNameErrorMessage"
@ -17,10 +18,19 @@
:readonly="loading" :readonly="loading"
:label="$t('pbxConfig.name')" :label="$t('pbxConfig.name')"
@input="$v.data.name.$touch" @input="$v.data.name.$touch"
/> >
<q-input <template
v-slot:prepend
>
<q-icon
name="person"
/>
</template>
</csc-input>
<csc-input
v-model="data.extension" v-model="data.extension"
clearable clearable
dense
hide-bottom-space hide-bottom-space
:error="$v.data.extension.$error" :error="$v.data.extension.$error"
:error-message="extensionErrorMessage" :error-message="extensionErrorMessage"
@ -28,11 +38,19 @@
:readonly="loading" :readonly="loading"
:label="$t('pbxConfig.extension')" :label="$t('pbxConfig.extension')"
@input="$v.data.extension.$touch" @input="$v.data.extension.$touch"
/> >
<csc-change-password-form <template
ref="changePasswordForm" v-slot:prepend
:no-submit="true" >
@validation-succeeded="webPassValidationSucceeded" <q-icon
name="call"
/>
</template>
</csc-input>
<csc-input-password-retype
v-model="data.password"
:disable="loading"
dense
/> />
</div> </div>
<div <div
@ -41,6 +59,7 @@
<q-select <q-select
v-model="data.aliasNumbers" v-model="data.aliasNumbers"
clearable clearable
dense
multiple multiple
use-chips use-chips
emit-value emit-value
@ -53,6 +72,7 @@
<q-select <q-select
v-model="data.groups" v-model="data.groups"
clearable clearable
dense
multiple multiple
use-chips use-chips
emit-value emit-value
@ -61,53 +81,63 @@
:readonly="loading" :readonly="loading"
:label="$t('pbxConfig.groups')" :label="$t('pbxConfig.groups')"
:options="groupOptions" :options="groupOptions"
/> >
<template
v-slot:prepend
>
<q-icon
name="group"
/>
</template>
</q-select>
<q-select <q-select
v-model="data.soundSet" v-model="data.soundSet"
radio radio
dense
emit-value emit-value
map-options map-options
:disable="loading" :disable="loading"
:readonly="loading" :readonly="loading"
:label="$t('pbxConfig.soundSet')" :label="$t('pbxConfig.soundSet')"
:options="soundSetOptions" :options="soundSetOptions"
/> >
<template
v-slot:prepend
>
<q-icon
name="queue_music"
/>
</template>
</q-select>
<q-toggle <q-toggle
v-model="data.clirIntrapbx" v-model="data.clirIntrapbx"
:label="$t('pbxConfig.toggleIntraPbx')" :label="$t('pbxConfig.toggleIntraPbx')"
:disable="loading" :disable="loading"
class="q-pa-md" class="q-pa-md"
dense
/> />
</div> </div>
</div> </div>
<div <div
class="row justify-center" class="row justify-center"
> >
<div <q-btn
class="col col-4" flat
> color="default"
<q-btn icon="clear"
v-if="!loading" :disable="loading"
flat :label="$t('buttons.cancel')"
color="default" @click="cancel()"
icon="clear" />
:label="$t('buttons.cancel')" <q-btn
@click="cancel()" flat
/> color="primary"
<q-btn icon="person"
v-if="!loading" :loading="loading"
flat :disable="$v.data.$invalid || loading"
color="primary" :label="$t('pbxConfig.createSeat')"
icon="person" @click="save()"
:disable="$v.data.$invalid" />
:label="$t('pbxConfig.createSeat')"
@click="save()"
/>
<csc-object-spinner
v-if="loading"
:loading="loading"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -118,13 +148,13 @@ import {
maxLength, maxLength,
numeric numeric
} from 'vuelidate/lib/validators' } from 'vuelidate/lib/validators'
import CscObjectSpinner from '../../CscObjectSpinner' import CscInput from 'components/form/CscInput'
import CscChangePasswordForm from '../../form/CscChangePasswordForm' import CscInputPasswordRetype from 'components/form/CscInputPasswordRetype'
export default { export default {
name: 'CscPbxSeatAddForm', name: 'CscPbxSeatAddForm',
components: { components: {
CscObjectSpinner, CscInputPasswordRetype,
CscChangePasswordForm CscInput
}, },
props: { props: {
loading: { loading: {
@ -154,9 +184,6 @@ export default {
required, required,
numeric, numeric,
maxLength: maxLength(64) maxLength: maxLength(64)
},
webPassword: {
maxLength: maxLength(64)
} }
} }
}, },
@ -211,17 +238,6 @@ export default {
} else { } else {
return '' return ''
} }
},
seatModel () {
return {
name: this.data.name,
extension: this.data.extension,
webPassword: this.data.webPassword,
aliasNumbers: this.data.aliasNumbers,
groups: this.data.groups,
soundSet: this.data.soundSet,
clirIntrapbx: this.data.clirIntrapbx
}
} }
}, },
created () { created () {
@ -234,7 +250,10 @@ export default {
return { return {
name: '', name: '',
extension: '', extension: '',
webPassword: '', password: {
password: '',
passwordRetype: ''
},
aliasNumbers: [], aliasNumbers: [],
groups: [], groups: [],
soundSet: null, soundSet: null,
@ -245,15 +264,19 @@ export default {
this.$emit('cancel') this.$emit('cancel')
}, },
save () { save () {
this.$emit('save', this.seatModel) this.$emit('save', {
this.$refs.changePasswordForm.resetForm() name: this.data.name,
extension: this.data.extension,
webPassword: this.data.password.password,
aliasNumbers: this.data.aliasNumbers,
groups: this.data.groups,
soundSet: this.data.soundSet,
clirIntrapbx: this.data.clirIntrapbx
})
}, },
reset () { reset () {
this.data = this.getDefaults() this.data = this.getDefaults()
this.$v.$reset() this.$v.$reset()
},
webPassValidationSucceeded (data) {
this.data.webPassword = data.password
} }
} }
} }

@ -0,0 +1,135 @@
<template>
<div>
<div
class="row justify-center full-width q-gutter-x-sm q-pt-sm"
>
<div
class="col-md-2"
>
<q-select
v-model="filterType"
emit-value
map-options
dense
:options="filterTypeOptions"
:label="$t('pbxConfig.seatsFiltersFilterByLabel')"
/>
</div>
<div
class="col-md-2"
>
<q-input
v-model="typedFilter"
type="text"
dense
:disable="filterType === null"
:label="$t('pbxConfig.seatsFilterInputLabel')"
@keypress.enter="triggerFilter"
>
<template
v-slot:append
>
<q-btn
icon="search"
color="primary"
dense
flat
@click="triggerFilter"
/>
</template>
</q-input>
</div>
</div>
<div
class="row justify-center full-width q-gutter-x-sm q-pt-sm"
>
<div
class="col-md-4"
>
<q-chip
v-for="(filterItem, index) in filters"
:key="index"
:label="$t('pbxConfig.seatsFiltersTypes.' + filterItem.name) + ': ' + filterItem.value"
:disable="false"
icon="filter_alt"
removable
dense
color="primary"
text-color="dark"
@remove="removeFilter(filterItem.name)"
/>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
name: 'CscPbxSeatFilters',
data () {
return {
filterType: null,
typedFilter: '',
filters: []
}
},
computed: {
filterTypeOptions () {
return [
{
label: this.$t('pbxConfig.seatsFiltersTypes.display_name'),
value: 'display_name'
},
{
label: this.$t('pbxConfig.seatsFiltersTypes.pbx_extension'),
value: 'pbx_extension'
},
{
label: this.$t('pbxConfig.seatsFiltersTypes.primary_number'),
value: 'primary_number'
},
{
label: this.$t('pbxConfig.seatsFiltersTypes.alias_number'),
value: 'alias_number'
}
]
}
},
methods: {
triggerFilter () {
this.addFilter(this.filterType, this.typedFilter)
},
removeFilter (name) {
this.filters = this.filters.filter(item => item.name !== name)
this.filter()
},
removeFilters () {
if (this.filters.length > 0) {
this.filters = []
this.filter()
}
},
addFilter (name, value) {
const valueTrimmed = _.trim(value)
if (valueTrimmed) {
this.typedFilter = ''
this.filters = this.filters.filter(item => item.name !== name)
const filter = {
name: name,
value: valueTrimmed
}
this.filters.push(filter)
this.filter()
}
},
filter () {
const params = {}
this.filters.forEach(filter => {
params[filter.name] = filter.value
})
this.$emit('filter', params)
}
}
}
</script>

@ -1,111 +1,53 @@
<template> <template>
<csc-page <csc-page-sticky>
class="q-pa-lg" <template
> v-slot:header
<csc-list-actions
class="row justify-center q-mb-lg"
> >
<csc-list-action-button <q-btn
v-if="isSeatAddFormDisabled"
slot="slot1"
icon="add" icon="add"
color="primary" color="primary"
flat
:label="$t('pbxConfig.addSeat')" :label="$t('pbxConfig.addSeat')"
:disable="isSeatListRequesting" :disable="!isSeatAddFormDisabled"
@click="enableSeatAddForm" @click="openAddForm"
/> />
<csc-list-action-button <q-btn
v-if="!showFilters" v-if="!showFilters"
slot="slot2"
icon="filter_alt" icon="filter_alt"
color="primary" color="primary"
flat
:label="$t('pbxConfig.seatsFilters')" :label="$t('pbxConfig.seatsFilters')"
:disable="isSeatListRequesting" @click="openFilters"
@click="toggleFilters()" />
> <q-btn
{{ $t('pbxConfig.seatsFilters') }} v-if="showFilters"
</csc-list-action-button> icon="clear"
</csc-list-actions> color="negative"
<csc-pbx-seat-add-form flat
v-if="!isSeatAddFormDisabled" :label="$t('pbxConfig.closeFilters')"
ref="addForm" @click="closeFilters"
class="q-mb-lg" />
:loading="isSeatCreating" </template>
:group-options="getGroupOptions" <template
:alias-number-options="getNumberOptions" v-slot:toolbar
:sound-set-options="getSoundSetOptions"
@save="createSeat"
@cancel="disableSeatAddForm"
/>
<div
v-if="showFilters"
class="row justify-center q-mb-lg"
> >
<div <csc-pbx-seat-filters
class="col col-6" v-if="showFilters"
> ref="filters"
<q-select @filter="filterEvent"
v-model="filterType" />
emit-value <csc-pbx-seat-add-form
map-options v-if="!isSeatAddFormDisabled"
:options="filterTypes" ref="addForm"
:label="$t('pbxConfig.seatsFiltersFilterByLabel')" class="q-mb-lg"
/> :loading="isSeatCreating"
<q-input :group-options="getGroupOptions"
v-if="filterType" :alias-number-options="getNumberOptions"
ref="inputFilter" :sound-set-options="getSoundSetOptions"
type="text" @save="createSeat"
:value="typedFilter" @cancel="disableSeatAddForm"
:label="$t('pbxConfig.seatsFilterInputLabel')" />
@input="inputFilter" </template>
>
<template
v-slot:append
>
<q-btn
icon="search"
color="primary"
dense
flat
/>
</template>
</q-input>
<div
class="q-mb-md"
>
<template
v-for="(filter, index) in filters"
>
<q-chip
v-if="filterType"
:key="index"
:label="filterType === 'name' ? 'Name: ' + filter : filter"
closables
@close="removeFilter(filter)"
/>
</template>
</div>
<div
class="row justify-center"
>
<q-btn
class="q-mr-sm"
flat
icon="clear"
color="white"
:label="$t('pbxConfig.seatsFiltersClose')"
@click="closeFilters"
/>
<q-btn
flat
icon="undo"
color="white"
:label="$t('pbxConfig.seatsFiltersReset')"
@click="emptyFilters"
/>
</div>
</div>
</div>
<div <div
v-if="isSeatListPaginationActive" v-if="isSeatListPaginationActive"
class="row justify-center" class="row justify-center"
@ -166,16 +108,14 @@
@remove="removeSeat({seatId:seatRemoving.id})" @remove="removeSeat({seatId:seatRemoving.id})"
@cancel="closeSeatRemovalDialog" @cancel="closeSeatRemovalDialog"
/> />
</csc-page> </csc-page-sticky>
</template> </template>
<script> <script>
import CscPage from '../../CscPage' import _ from 'lodash'
import CscPbxSeatAddForm from './CscPbxSeatAddForm' import CscPbxSeatAddForm from './CscPbxSeatAddForm'
import CscPbxSeat from './CscPbxSeat' import CscPbxSeat from './CscPbxSeat'
import CscRemoveDialog from '../../CscRemoveDialog' import CscRemoveDialog from '../../CscRemoveDialog'
import CscListActions from '../../CscListActions'
import CscListActionButton from '../../CscListActionButton'
import { import {
mapState, mapState,
mapGetters, mapGetters,
@ -193,17 +133,20 @@ import {
} from 'src/store/common' } from 'src/store/common'
import platform from '../../../mixins/platform' import platform from '../../../mixins/platform'
import CscList from '../../CscList' import CscList from '../../CscList'
import CscPageSticky from 'components/CscPageSticky'
import CscPbxSeatFilters from 'components/pages/PbxConfiguration/CscPbxSeatFilters'
export default { export default {
components: { components: {
CscPbxSeatFilters,
CscPageSticky,
CscSpinner, CscSpinner,
CscPage,
CscPbxSeat, CscPbxSeat,
CscPbxSeatAddForm, CscPbxSeatAddForm,
CscRemoveDialog, CscRemoveDialog,
CscList, CscList
CscListActions, // CscListActions,
CscListActionButton // CscListActionButton
}, },
mixins: [ mixins: [
platform platform
@ -211,12 +154,7 @@ export default {
data () { data () {
return { return {
showFilters: false, showFilters: false,
filterType: null, filters: null
filterTypes: [
{ label: this.$t('pbxConfig.seatsFiltersTypes.name'), value: 'name' }
],
typedFilter: null,
filters: []
} }
}, },
computed: { computed: {
@ -337,65 +275,30 @@ export default {
page: page page: page
}) })
}, },
toggleFilters () { openAddForm () {
this.showFilters = !this.showFilters this.enableSeatAddForm()
this.closeFilters()
},
openFilters () {
this.showFilters = true
this.disableSeatAddForm()
}, },
inputFilter (input) { inputFilter (input) {
this.typedFilter = input this.typedFilter = input
}, },
closeFilters () { closeFilters () {
if (this.$refs.filters) {
this.$refs.filters.removeFilters()
}
this.showFilters = false this.showFilters = false
}, },
emptyFilters () { filterEvent (filters) {
this.filterType = null
this.typedFilter = null
this.filters = []
this.$scrollTo(this.$parent.$el)
this.loadSeatListItems({
page: 1
})
},
triggerFilter () {
this.$scrollTo(this.$parent.$el) this.$scrollTo(this.$parent.$el)
this.loadSeatListItems({ this.filters = filters
page: 1, const payload = _.cloneDeep(filters)
display_name: this.typedFilter payload.page = 1
}) this.loadSeatListItems(payload)
this.filters = []
this.filters.push(this.typedFilter)
this.typedFilter = null
},
removeFilter (filter) {
this.filters = this.filters.filter($filter => $filter !== filter)
if (this.filters.length < 1) {
this.emptyFilters()
}
} }
} }
} }
</script> </script>
<style lang="stylus" rel="stylesheet/stylus">
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.csc-pbx-filters-container
color $secondary
margin-bottom 20px
.csc-pbx-chips-container
margin 20px auto 20px auto
text-align center
.csc-pbx-filters-field
width 250px
display inline-block
margin-left 10px
.csc-pbx-filter-fields-container
margin-top -15px
.csc-pbx-filter-buttons
margin-top 15px
text-align center
</style>

@ -53,12 +53,6 @@ export default {
isLoading: false isLoading: false
} }
}, },
async mounted () {
this.requestInProgress(true)
const preferences = await this.loadPreferences(getSubscriberId())
this.clirIntrapbx = preferences.clir_intrapbx
this.requestInProgress(false)
},
computed: { computed: {
...mapGetters('pbxSeats', [ ...mapGetters('pbxSeats', [
'getIntraPbx' 'getIntraPbx'
@ -85,6 +79,12 @@ export default {
} }
} }
}, },
async mounted () {
this.requestInProgress(true)
const preferences = await this.loadPreferences(getSubscriberId())
this.clirIntrapbx = preferences.clir_intrapbx
this.requestInProgress(false)
},
methods: { methods: {
...mapActions('pbxSeats', [ ...mapActions('pbxSeats', [
'setIntraPbx', 'setIntraPbx',

@ -602,12 +602,16 @@
"seatsFilters": "Filter", "seatsFilters": "Filter",
"seatsFiltersFilterByLabel": "Filter by", "seatsFiltersFilterByLabel": "Filter by",
"seatsFiltersTypes": { "seatsFiltersTypes": {
"name": "Name" "display_name": "Name",
"pbx_extension": "Extension",
"primary_number": "Primary Number",
"alias_number": "Alias Number"
}, },
"seatsFiltersSearch": "Search", "seatsFiltersSearch": "Search",
"seatsFiltersClose": "Close", "seatsFiltersClose": "Close",
"seatsFiltersReset": "Reset Filters", "seatsFiltersReset": "Reset Filters",
"seatsFilterInputLabel": "Type something" "seatsFilterInputLabel": "Type something",
"webusername": "Login"
}, },
"callBlocking": { "callBlocking": {
"privacyEnabledToast": "Your number is hidden to the callee", "privacyEnabledToast": "Your number is hidden to the callee",

@ -256,12 +256,18 @@ export default {
const page = _.get(options, 'page', context.state.seatListCurrentPage) const page = _.get(options, 'page', context.state.seatListCurrentPage)
const clearList = _.get(options, 'clearList', true) const clearList = _.get(options, 'clearList', true)
const displayName = _.get(options, 'display_name', null) const displayName = _.get(options, 'display_name', null)
const pbxExtension = _.get(options, 'pbx_extension', null)
const primaryNumber = _.get(options, 'primary_number', null)
const aliasNumber = _.get(options, 'alias_number', null)
context.commit('seatListItemsRequesting', { context.commit('seatListItemsRequesting', {
clearList: clearList clearList: clearList
}) })
getSeatList({ getSeatList({
page: page, page: page,
display_name: displayName display_name: displayName,
pbx_extension: pbxExtension,
primary_number: primaryNumber,
alias_number: aliasNumber
}).then((seatList) => { }).then((seatList) => {
context.commit('pbx/pilotSucceeded', seatList.pilot, { root: true }) context.commit('pbx/pilotSucceeded', seatList.pilot, { root: true })
context.commit('pbx/numbersSucceeded', seatList.numbers, { root: true }) context.commit('pbx/numbersSucceeded', seatList.numbers, { root: true })

@ -3,7 +3,7 @@ import Vue from 'vue'
import numberFilter from '../filters/number' import numberFilter from '../filters/number'
import _ from 'lodash' import _ from 'lodash'
import { import {
getAllProfiles getAllProfiles, getModel, getModelFrontImage, getModelFrontThumbnailImage
} from '../api/pbx-config' } from '../api/pbx-config'
import { import {
getSubscribers getSubscribers
@ -11,9 +11,9 @@ import {
import { import {
RequestState RequestState
} from './common' } from './common'
import { // import {
loadDeviceModel // loadDeviceModel
} from '../api/pbx-devices' // } from '../api/pbx-devices'
import { getNumbers } from '../api/user' import { getNumbers } from '../api/user'
import { import {
i18n i18n
@ -37,6 +37,7 @@ export default {
deviceModelList: [], deviceModelList: [],
deviceModelMap: {}, deviceModelMap: {},
deviceModelImageMap: {}, deviceModelImageMap: {},
deviceModelImageSmallMap: {},
subscriberList: [], subscriberList: [],
subscriberListState: RequestState.initiated, subscriberListState: RequestState.initiated,
subscriberMap: {} subscriberMap: {}
@ -193,16 +194,21 @@ export default {
deviceModelSucceeded (state, deviceModel) { deviceModelSucceeded (state, deviceModel) {
const model = _.get(deviceModel, 'model', null) const model = _.get(deviceModel, 'model', null)
const modelImage = _.get(deviceModel, 'modelImage', null) const modelImage = _.get(deviceModel, 'modelImage', null)
const modelImageThumbnail = _.get(deviceModel, 'modelImageThumbnail', null)
if (model !== null) { if (model !== null) {
Vue.set(state.deviceModelMap, deviceModel.model.id, deviceModel.model) Vue.set(state.deviceModelMap, model.id, model)
} }
if (modelImage !== null) { if (modelImage !== null) {
Vue.set(state.deviceModelImageMap, deviceModel.modelImage.id, deviceModel.modelImage) Vue.set(state.deviceModelImageMap, modelImage.id, modelImage)
}
if (modelImageThumbnail !== null) {
Vue.set(state.deviceModelImageSmallMap, modelImageThumbnail.id, modelImageThumbnail)
} }
}, },
deviceModelFailed (state, deviceModelId) { deviceModelFailed (state, deviceModelId) {
Vue.delete(state.deviceModelMap, deviceModelId) Vue.delete(state.deviceModelMap, deviceModelId)
Vue.delete(state.deviceModelImageMap, deviceModelId) Vue.delete(state.deviceModelImageMap, deviceModelId)
Vue.delete(state.deviceModelImageSmallMap, deviceModelId)
}, },
subscribersRequesting (state) { subscribersRequesting (state) {
state.subcriberListState = RequestState.requesting state.subcriberListState = RequestState.requesting
@ -232,19 +238,69 @@ export default {
} }
}) })
}, },
loadDeviceModel (context, deviceModelId) { async loadDeviceModel (context, payload) {
if (!context.state.deviceModelMap[deviceModelId]) { try {
loadDeviceModel(deviceModelId).then((deviceModel) => { const isFrontCached = context.state.deviceModelImageMap[payload.deviceId] !== undefined
context.commit('deviceModelSucceeded', deviceModel) const isFrontThumbnailCached = context.state.deviceModelImageSmallMap[payload.deviceId] !== undefined
}).catch(() => { const isModelCached = context.state.deviceModelMap[payload.deviceId] !== undefined
context.commit('deviceModelFailed', deviceModelId) const deviceModel = {
}) modelImage: null,
modelImageThumbnail: null,
model: null
}
const requests = []
let isFrontImageRequested = false
if (!isFrontCached && (payload.type === 'front' || payload.type === 'all')) {
requests.push(getModelFrontImage(payload.deviceId))
isFrontImageRequested = true
}
let isFrontThumbnailImageRequested = false
if (!isFrontThumbnailCached && (payload.type === 'front_thumb' || payload.type === 'all')) {
requests.push(getModelFrontThumbnailImage(payload.deviceId))
isFrontThumbnailImageRequested = true
}
let isModelRequested = false
if (!isModelCached) {
requests.push(getModel(payload.deviceId))
isModelRequested = true
}
if (requests.length > 0) {
const res = await Promise.all(requests)
if (res.length === 1 && isModelRequested) {
deviceModel.model = res[0]
} else if (res.length === 1 && isFrontImageRequested) {
deviceModel.modelImage = res[0]
} else if (res.length === 1 && isFrontThumbnailImageRequested) {
deviceModel.modelImageThumbnail = res[0]
} else if (res.length === 2 && isModelRequested && isFrontImageRequested) {
deviceModel.modelImage = res[0]
deviceModel.model = res[1]
} else if (res.length === 2 && isModelRequested && isFrontThumbnailImageRequested) {
deviceModel.modelImageThumbnail = res[0]
deviceModel.model = res[1]
} else if (res.length === 2 && isFrontImageRequested && isFrontThumbnailImageRequested) {
deviceModel.modelImage = res[0]
deviceModel.modelImageThumbnail = res[1]
} else if (res.length === 3) {
deviceModel.modelImage = res[0]
deviceModel.modelImageThumbnail = res[1]
deviceModel.model = res[2]
}
}
context.commit('deviceModelSucceeded', deviceModel)
} catch (err) {
context.commit('deviceModelFailed', payload.deviceId)
} }
}, },
loadDeviceModels (context) { async loadDeviceModels (context, imageType) {
context.state.deviceProfileList.forEach((profile) => { const requests = []
context.dispatch('loadDeviceModel', profile.device_id) for (let i = 0; i < context.state.deviceProfileList.length; i++) {
}) requests.push(context.dispatch('loadDeviceModel', {
deviceId: context.state.deviceProfileList[i].device_id,
type: imageType
}))
}
await Promise.all(requests)
}, },
loadSubscribers (context) { loadSubscribers (context) {
if (context.state.subscriberList.length === 0 && if (context.state.subscriberList.length === 0 &&

@ -5457,6 +5457,11 @@ gaze@^1.0.0:
dependencies: dependencies:
globule "^1.0.0" globule "^1.0.0"
generate-password@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/generate-password/-/generate-password-1.5.1.tgz#ad463fadee1b4818edb7b827ff6f3499587d8dd5"
integrity sha512-XdsyfiF4mKoOEuzA44w9jSNav50zOurdWOV3V8DbA7SJIxR3Xm9ob14HKYTnMQOPX3ylqiJMnQF0wEa8gXZIMw==
gensync@^1.0.0-beta.1: gensync@^1.0.0-beta.1:
version "1.0.0-beta.1" version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"

Loading…
Cancel
Save