From 6df2d69eb856ba1cdcbab5aa2ef70405c17c742f Mon Sep 17 00:00:00 2001 From: Sergii Leonenko <sleonenko@sipwise.com> Date: Mon, 7 Dec 2020 22:59:05 +0200 Subject: [PATCH] TT#96352 - CSC: As a Customer, I want change my Mail2Fax settings AC: If not already exists: 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 see settings if the feature enabled Can decide either to use SecretKey or ACL to manage authentication Can set a custom secret key/token Can set the renew interval (never, daily, weekly, monthly) Can see/read the "Last Secret Key Modify Time" Can add email addresses to get notified about expired key (secret_renew_notify) Can remove email addresses Can add ACL Rule (email, ip, destination, use-regexp flag) Can edit ACL Rule (email, ip, destination, use-regexp flag) Can remove ACL Rule Change-Id: I6bc25ab2f73d0dfae3fab224b11396ecdd17ab39 --- src/api/fax.js | 27 ++ src/boot/i18n.js | 1 + src/components/CscMainMenuTop.vue | 5 +- src/components/CscTooltip.vue | 55 +++ src/components/form/CscInputSaveable.vue | 1 + .../CallForward/CscAddDestinationForm.vue | 4 +- ...nation.vue => CscFaxToMailDestination.vue} | 8 +- ...rm.vue => CscFaxToMailDestinationForm.vue} | 4 +- .../FaxSettings/CscFaxToMailSettings.vue | 272 +++++++++++++ .../pages/FaxSettings/CscMailToFaxACL.vue | 109 +++++ .../pages/FaxSettings/CscMailToFaxACLForm.vue | 177 +++++++++ .../CscMailToFaxRenewNotifyEmail.vue | 152 +++++++ .../CscMailToFaxRenewNotifyEmailForm.vue | 129 ++++++ .../FaxSettings/CscMailToFaxSettings.vue | 373 ++++++++++++++++++ src/css/app.styl | 6 + src/i18n/en.json | 15 +- src/layouts/CscLayoutMain.vue | 6 +- src/pages/CscPageFaxSettings.vue | 291 ++------------ src/plugins/rtc-engine.js | 28 +- src/router/routes.js | 8 + src/store/call-forward.js | 4 +- src/store/fax.js | 33 +- src/store/user.js | 6 +- 23 files changed, 1422 insertions(+), 292 deletions(-) create mode 100644 src/components/CscTooltip.vue rename src/components/pages/FaxSettings/{CscFax2MailDestination.vue => CscFaxToMailDestination.vue} (91%) rename src/components/pages/FaxSettings/{CscFax2MailDestinationForm.vue => CscFaxToMailDestinationForm.vue} (98%) create mode 100644 src/components/pages/FaxSettings/CscFaxToMailSettings.vue create mode 100644 src/components/pages/FaxSettings/CscMailToFaxACL.vue create mode 100644 src/components/pages/FaxSettings/CscMailToFaxACLForm.vue create mode 100644 src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue create mode 100644 src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue create mode 100644 src/components/pages/FaxSettings/CscMailToFaxSettings.vue diff --git a/src/api/fax.js b/src/api/fax.js index 10215509..946569b2 100644 --- a/src/api/fax.js +++ b/src/api/fax.js @@ -32,3 +32,30 @@ export async function setFaxServerField (options) { value: options.value }) } + +export async function getMailToFaxSettings (subscriberId) { + const result = await get({ + path: `api/mailtofaxsettings/${subscriberId}` + }) + const settings = _.clone(result) + delete settings._links + return settings +} + +export async function setMailToFaxSettingField (options) { + if (!['active', 'secret_key', 'secret_key_renew', 'secret_renew_notify', 'acl'].includes(options.field)) { + throw Error(`setMailToFaxSettingField: unknown field name ${options.field}`) + } + if (options.field === 'secret_renew_notify') { + // searching for duplicates + const destinationsIds = options.value.map(d => d.destination) + if ((new Set(destinationsIds)).size !== destinationsIds.length) { + throw Error(i18n.t('faxSettings.notifyEmailExists')) + } + } + return patchReplaceFull({ + path: `api/mailtofaxsettings/${options.subscriberId}`, + fieldPath: options.field, + value: options.value + }) +} diff --git a/src/boot/i18n.js b/src/boot/i18n.js index e8e7a3fa..61b5dccf 100644 --- a/src/boot/i18n.js +++ b/src/boot/i18n.js @@ -17,6 +17,7 @@ export const defaultLocale = 'en-US' export const i18n = new VueI18n({ locale: defaultLocale, fallbackLocale: defaultLocale, + formatFallbackMessages: true, messages }) diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue index 2b8077d3..cebe6413 100644 --- a/src/components/CscMainMenuTop.vue +++ b/src/components/CscMainMenuTop.vue @@ -46,7 +46,8 @@ export default { computed: { ...mapGetters('user', [ 'isRtcEngineUiVisible', - 'isPbxEnabled' + 'isPbxEnabled', + 'hasFaxCapability' ]), items () { return [ @@ -129,7 +130,7 @@ export default { to: '/user/fax-settings', icon: 'fas fa-fax', label: this.$t('navigation.faxSettings.title'), - visible: true + visible: this.hasFaxCapability }, { icon: 'miscellaneous_services', diff --git a/src/components/CscTooltip.vue b/src/components/CscTooltip.vue new file mode 100644 index 00000000..a6a2e80f --- /dev/null +++ b/src/components/CscTooltip.vue @@ -0,0 +1,55 @@ +<template> + <q-tooltip + ref="tooltip" + :delay="delay" + :content-class="contentClass" + v-bind="$attrs" + v-on="$listeners" + @show="autoHide" + @hide="cancelAutoHide" + > + <slot /> + </q-tooltip> +</template> + +<script> +export default { + name: 'CscTooltip', + props: { + autoHideDelay: { + type: Number, + default: 5000 + }, + delay: { + type: Number, + default: 500 + }, + contentClass: { + type: String, + default: 'text-dark' + } + }, + data () { + return { + autoHideHandler: undefined + } + }, + beforeDestroy () { + this.cancelAutoHide() + }, + methods: { + autoHide () { + this.cancelAutoHide() + this.autoHideHandler = setTimeout(() => { + this.autoHideHandler = undefined + this.$refs.tooltip.hide() + }, + this.autoHideDelay + ) + }, + cancelAutoHide () { + clearTimeout(this.autoHideHandler) + } + } +} +</script> diff --git a/src/components/form/CscInputSaveable.vue b/src/components/form/CscInputSaveable.vue index 4c78f4e8..c407819f 100644 --- a/src/components/form/CscInputSaveable.vue +++ b/src/components/form/CscInputSaveable.vue @@ -7,6 +7,7 @@ @input="$emit('input', $event)" @keyup.enter="$emit('save', $event)" > + <slot /> <template v-if="icon !== undefined && icon !== null" v-slot:prepend diff --git a/src/components/pages/CallForward/CscAddDestinationForm.vue b/src/components/pages/CallForward/CscAddDestinationForm.vue index da262aed..1cbdc4ec 100644 --- a/src/components/pages/CallForward/CscAddDestinationForm.vue +++ b/src/components/pages/CallForward/CscAddDestinationForm.vue @@ -18,7 +18,7 @@ @click="addDestinationByType('voicebox')" /> <csc-popup-menu-item - v-if="hasFaxCapability && hasSendFaxFeature" + v-if="hasFaxCapabilityAndFaxActive && hasSendFaxFeature" :label="$t('pages.callForward.buttons.addFax2Mail')" @click="addDestinationByType('fax2mail')" /> @@ -150,7 +150,7 @@ export default { ]), ...mapGetters('user', [ 'hasSendFaxFeature', - 'hasFaxCapability' + 'hasFaxCapabilityAndFaxActive' ]), timeoutInputError () { if (!this.$v.destinationForm.timeout.required) { diff --git a/src/components/pages/FaxSettings/CscFax2MailDestination.vue b/src/components/pages/FaxSettings/CscFaxToMailDestination.vue similarity index 91% rename from src/components/pages/FaxSettings/CscFax2MailDestination.vue rename to src/components/pages/FaxSettings/CscFaxToMailDestination.vue index b9ba9cc1..a8f79570 100644 --- a/src/components/pages/FaxSettings/CscFax2MailDestination.vue +++ b/src/components/pages/FaxSettings/CscFaxToMailDestination.vue @@ -43,7 +43,7 @@ </csc-list-menu-item> </template> <template slot="body"> - <csc-fax2-mail-destination-form + <csc-fax-to-mail-destination-form :is-add-new-mode="false" :initial-data="destination" :loading="loading" @@ -58,12 +58,12 @@ 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' +import CscFaxToMailDestinationForm from 'components/pages/FaxSettings/CscFaxToMailDestinationForm' export default { - name: 'CscFax2MailDestination', + name: 'CscFaxToMailDestination', components: { - CscFax2MailDestinationForm, + CscFaxToMailDestinationForm, CscListItemSubtitle, CscListMenuItem, CscListItemTitle, diff --git a/src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue b/src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue similarity index 98% rename from src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue rename to src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue index 4a39406d..7ae18398 100644 --- a/src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue +++ b/src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue @@ -70,7 +70,7 @@ <q-btn flat color="primary" - icon="person" + icon="done" :loading="loading" :disable="$v.data.$invalid || loading" :label="$t('faxSettings.createDestination')" @@ -85,7 +85,7 @@ import { email, required } from 'vuelidate/lib/validators' import CscInputSaveable from 'components/form/CscInputSaveable' export default { - name: 'CscFax2MailDestinationForm', + name: 'CscFaxToMailDestinationForm', components: { CscInputSaveable }, diff --git a/src/components/pages/FaxSettings/CscFaxToMailSettings.vue b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue new file mode 100644 index 00000000..6a85d590 --- /dev/null +++ b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue @@ -0,0 +1,272 @@ +<template> + <div> + <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">{{ $t('faxSettings.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-fax-to-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-fax-to-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> + </div> +</template> + +<script> +import _ from 'lodash' +import { mapState } from 'vuex' +import CscInputSaveable from 'components/form/CscInputSaveable' +import CscSpinner from 'components/CscSpinner' +import { mapWaitingActions, mapWaitingGetters } from 'vue-wait' +import CscFaxToMailDestinationForm from 'components/pages/FaxSettings/CscFaxToMailDestinationForm' +import CscFaxToMailDestination from 'components/pages/FaxSettings/CscFaxToMailDestination' +import CscRemoveDialog from 'components/CscRemoveDialog' +import { showGlobalError } from 'src/helpers/ui' +export default { + name: 'CscFaxToMailSettings', + components: { + CscFaxToMailDestination, + CscFaxToMailDestinationForm, + CscSpinner, + 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', + faxServerSettingsUpdateAction: '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.faxServerSettingsUpdateAction({ field, value }) + this.updateDataFromStore() + } catch (err) { + showGlobalError(err?.message) + } + }, + restoreName () { + this.faxToMailSettings.name = this.faxServerSettings.name + }, + async updateDestinations (destinationItems, beforeUpdateUI = () => {}) { + try { + await this.faxServerSettingsUpdateAction({ + 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.faxServerSettingsUpdateAction({ + 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> diff --git a/src/components/pages/FaxSettings/CscMailToFaxACL.vue b/src/components/pages/FaxSettings/CscMailToFaxACL.vue new file mode 100644 index 00000000..f56097a0 --- /dev/null +++ b/src/components/pages/FaxSettings/CscMailToFaxACL.vue @@ -0,0 +1,109 @@ +<template> + <div> + <div class="q-item"> + <div + class="csc-list-item-head row items-center" + @click="toggle" + > + <div + class="q-item__section column q-item__section--side justify-center" + > + <q-icon + name="fas fa-shield-alt" + size="24px" + :color="expanded ? 'primary' : ''" + /> + </div> + <div + class="q-item__section column q-item__section--main justify-center" + :class="expanded ? 'text-primary' : ''" + > + <div class="q-item__label text-caption"> + <u>{{ acl.from_email }}</u> and + <u>{{ acl.received_from }} </u> <sup v-if="acl.use_regex">(.*) </sup> => + <u>{{ acl.destination }} </u> <sup v-if="acl.use_regex">(.*) </sup> + </div> + </div> + <div + class="q-item__section column q-item__section--side justify-center" + > + <q-btn + flat + dense + icon="delete" + text-color="negative" + :title="$t('Remove')" + :disable="isChanged" + @click.stop="remove" + /> + </div> + </div> + <q-slide-transition> + <div + v-if="expanded" + class="csc-list-item-body" + > + <csc-mail-to-fax-a-c-l-form + :is-add-new-mode="false" + :initial-data="acl" + :loading="loading" + @update-property="updateProperty" + /> + </div> + </q-slide-transition> + </div> + </div> +</template> + +<script> +import CscMailToFaxACLForm from 'components/pages/FaxSettings/CscMailToFaxACLForm' + +export default { + name: 'CscMailToFaxACL', + components: { + CscMailToFaxACLForm + }, + props: { + acl: { + type: Object, + required: true + }, + expanded: { + type: Boolean, + default: false + }, + loading: { + type: Boolean, + default: false + } + }, + computed: { + isChanged () { + return false + } + }, + methods: { + toggle () { + if (this.expanded) { + this.$emit('collapse') + } else { + this.$emit('expand') + } + }, + updateProperty () { + this.$emit('update-property', ...arguments) + }, + remove () { + this.$q.dialog({ + title: this.$t('Remove ACL'), + message: this.$t('You are about to remove ACL: From email <{from_email}>', { from_email: this.acl.from_email }), + color: 'primary', + cancel: true, + persistent: true + }).onOk(() => { + this.$emit('remove', this.key) + }) + } + } +} +</script> diff --git a/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue b/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue new file mode 100644 index 00000000..ab84293b --- /dev/null +++ b/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue @@ -0,0 +1,177 @@ +<template> + <div> + <div class="row"> + <div + class="col" + > + <csc-input-saveable + v-model="data.from_email" + icon="email" + :label="$t('From email')" + :disable="disabled" + :readonly="loading" + :error="$v.data.from_email.$error" + :error-message="fromEmailErrorMessage" + :value-changed="!isAddNewMode && data.from_email !== initialData.from_email" + @input="$v.data.from_email.$touch" + @keypress.space.prevent + @keydown.space.prevent + @keyup.space.prevent + @undo="data.from_email = initialData.from_email" + @save="updatePropertyData('from_email')" + > + <csc-tooltip> + {{ $t('Accepted email address to allow mail2fax transmission.') }} + </csc-tooltip> + </csc-input-saveable> + <csc-input-saveable + v-model="data.received_from" + :label="$t('Received from IP')" + :disable="disabled" + :readonly="loading" + :value-changed="!isAddNewMode && data.received_from !== initialData.received_from" + @keypress.space.prevent + @keydown.space.prevent + @keyup.space.prevent + @undo="data.received_from = initialData.received_from" + @save="updatePropertyData('received_from')" + > + <csc-tooltip> + {{ $t('Allow mail2fax emails only to this IP (the IP or hostname is present in the "Received" header).') }} + </csc-tooltip> + </csc-input-saveable> + <csc-input-saveable + v-model="data.destination" + :label="$t('Destination')" + :disable="disabled" + :readonly="loading" + :value-changed="!isAddNewMode && data.destination !== initialData.destination" + @keypress.space.prevent + @keydown.space.prevent + @keyup.space.prevent + @undo="data.destination = initialData.destination" + @save="updatePropertyData('destination')" + > + <csc-tooltip> + {{ $t('Allow mail2fax destination only to this number.') }} + </csc-tooltip> + </csc-input-saveable> + + <q-toggle + v-model="data.use_regex" + :label="$t('Use RegExp')" + :hint="$t('Enable regex matching for "Received from IP" and "Destination" fields.')" + :disable="loading" + @input="updatePropertyData('use_regex')" + > + <csc-tooltip> + {{ $t('Enable regex matching for "Received from IP" and "Destination" fields.') }} + </csc-tooltip> + </q-toggle> + </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="Create ACL" + @click="save()" + /> + </div> + </div> +</template> + +<script> +import { email } from 'vuelidate/lib/validators' +import CscInputSaveable from 'components/form/CscInputSaveable' +import CscTooltip from 'components/CscTooltip' + +export default { + name: 'CscMailToFaxACLForm', + components: { + CscTooltip, + CscInputSaveable + }, + props: { + loading: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + initialData: { + type: Object, + default: () => ({ + destination: '', + from_email: '', + received_from: '', + use_regex: false + }) + }, + isAddNewMode: { + type: Boolean, + default: false + } + }, + data () { + return { + data: this.getDefaults() + } + }, + validations: { + data: { + from_email: { + email + } + } + }, + computed: { + fromEmailErrorMessage () { + if (!this.$v.data.from_email.email) { + return this.$t('validationErrors.email') + } else { + return '' + } + } + }, + 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> diff --git a/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue new file mode 100644 index 00000000..19a8936c --- /dev/null +++ b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue @@ -0,0 +1,152 @@ +<template> + <q-item> + <q-item-section + v-if="!editing" + side + @click="activateEditing" + > + <q-icon name="email" /> + </q-item-section> + <q-item-section + @click="activateEditing" + > + <q-item-label + v-if="!editing" + > + {{ value }} + </q-item-label> + <csc-input-saveable + v-else + ref="emailInput" + v-model="newEmail" + :label="$t('Renew Notify Email')" + :value-changed="isChanged" + :error="$v.newEmail.$error" + :error-message="newEmailErrorMessage" + dense + @keypress.space.prevent + @keydown.space.prevent + @keyup.space.prevent + @input="$v.newEmail.$touch" + @save="save" + @undo="undo" + @focusout="focusOutEditing" + @focusin="cancelTimer" + /> + </q-item-section> + <q-item-section + side + > + <q-btn + flat + dense + icon="delete" + text-color="negative" + :title="$t('Remove')" + :disable="isChanged" + @click="remove" + /> + </q-item-section> + </q-item> +</template> + +<script> +import CscInputSaveable from 'components/form/CscInputSaveable' +import { email, required } from 'vuelidate/lib/validators' +export default { + name: 'CscMailToFaxRenewNotifyEmail', + components: { + CscInputSaveable + }, + props: { + value: { + type: String, + required: true + } + }, + data () { + return { + newEmail: this.value, + editing: false, + timerHandler: undefined + } + }, + validations: { + newEmail: { + required, + email + } + }, + computed: { + isChanged () { + return this.newEmail !== this.value + }, + newEmailErrorMessage () { + if (!this.$v.newEmail.required) { + return this.$t('validationErrors.fieldRequired', { + field: this.$t('Renew Notify Email') + }) + } else if (!this.$v.newEmail.email) { + return this.$t('validationErrors.email') + } else { + return '' + } + } + }, + beforeDestroy () { + this.cancelTimer() + }, + methods: { + activateEditing () { + if (!this.editing) { + this.newEmail = this.value + this.editing = true + this.focusEmailInput() + } + }, + deactivateEditing () { + this.timerHandler = setTimeout(() => { + this.editing = false + }, 1000) + }, + cancelTimer () { + clearTimeout(this.timerHandler) + }, + focusOutEditing () { + if (!this.isChanged) { + this.deactivateEditing() + } + }, + focusEmailInput () { + this.$nextTick(() => { + const emailInput = this.$refs.emailInput?.$el + if (emailInput) { + emailInput.focus() + } + }) + }, + undo () { + this.newEmail = this.value + this.$v.$reset() + this.focusEmailInput() + }, + save () { + this.$emit('save', { + id: this.key, + value: this.newEmail + }) + }, + remove () { + this.$q.dialog({ + title: this.$t('faxSettings.deleteRenewNotifyEmailTitle'), + message: this.$t('faxSettings.deleteRenewNotifyEmailText', { email: this.value }), + color: 'primary', + cancel: true, + persistent: true + }).onOk(() => { + this.$emit('remove', this.key) + }) + } + } +} +</script> diff --git a/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue new file mode 100644 index 00000000..ad0be762 --- /dev/null +++ b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue @@ -0,0 +1,129 @@ +<template> + <div class="csc-form"> + <csc-input-saveable + v-model="data.destination" + icon="email" + :label="$t('Renew Notify Email')" + :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')" + > + <csc-tooltip> + {{ $t('Destination email to send the secret key renew notification to.') }} + </csc-tooltip> + </csc-input-saveable> + <div + v-if="isAddNewMode" + class="csc-form-actions row justify-center" + > + <q-btn + flat + color="default" + icon="clear" + :disable="loading" + :label="$t('Cancel')" + @click="cancel()" + /> + <q-btn + flat + color="primary" + icon="done" + :loading="loading" + :disable="$v.data.$invalid || loading" + :label="$t('Add email')" + @click="save()" + /> + </div> + </div> +</template> + +<script> +import { email, required } from 'vuelidate/lib/validators' +import CscInputSaveable from 'components/form/CscInputSaveable' +import CscTooltip from 'components/CscTooltip' + +export default { + name: 'CscMailToFaxRenewNotifyEmailForm', + components: { + CscTooltip, + CscInputSaveable + }, + props: { + loading: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + initialData: { + type: Object, + default: () => ({ + destination: '' + }) + }, + 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('Email') + }) + } else if (!this.$v.data.destination.email) { + return this.$t('validationErrors.email') + } else { + return '' + } + } + }, + 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> diff --git a/src/components/pages/FaxSettings/CscMailToFaxSettings.vue b/src/components/pages/FaxSettings/CscMailToFaxSettings.vue new file mode 100644 index 00000000..a98cdc65 --- /dev/null +++ b/src/components/pages/FaxSettings/CscMailToFaxSettings.vue @@ -0,0 +1,373 @@ +<template> + <div + v-if="!mailToFaxSettingsModel.active" + class="q-pa-md" + > + <csc-spinner + v-if="loadingMail2FaxSettings" + class="self-center" + /> + <div v-else> + {{ $t('faxSettings.featureIsNotActive') }} + </div> + </div> + <div v-else> + <q-list + class="col col-xs-12 col-md-6" + dense + > + <q-item> + <q-item-section> + <q-toggle + :value="mailToFaxSettingsModel.active" + :label="$t('faxSettings.active')" + :disable="true" + /> + </q-item-section> + <q-item-section + side + > + <csc-spinner + v-if="loadingMail2FaxSettings" + class="self-center" + /> + </q-item-section> + </q-item> + <q-item> + <q-item-section> + <csc-input-saveable + v-model.trim="mailToFaxSettingsModel.secret_key" + :label="secretKeyFieldLabel" + :disable="!dataLoaded" + :loading="loadingMail2FaxSettings" + :value-changed="mailToFaxSettingsModel.secret_key !== mailToFaxSettings.secret_key" + @save="setChangedData('secret_key', mailToFaxSettingsModel.secret_key)" + @undo="mailToFaxSettingsModel.secret_key = mailToFaxSettings.secret_key" + > + <csc-tooltip> + {{ $t('Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.') }} + </csc-tooltip> + </csc-input-saveable> + </q-item-section> + </q-item> + <q-item> + <q-item-section> + <q-select + v-model="mailToFaxSettingsModel.secret_key_renew" + emit-value + map-options + :disable="!dataLoaded" + :readonly="!dataLoaded" + :label="$t('faxSettings.secretKeyRenew')" + :options="secretKeyRenewOptions" + @input="setChangedData('secret_key_renew', mailToFaxSettingsModel.secret_key_renew)" + > + <csc-tooltip> + {{ $t('Interval when the secret key is automatically renewed.') }} + </csc-tooltip> + </q-select> + </q-item-section> + <q-item-section + side + > + <csc-spinner + v-if="loadingMail2FaxSettings" + class="self-center" + /> + </q-item-section> + </q-item> + </q-list> + <div class="row"> + <div class="col q-py-md q-pl-md"> + <div class="row q-pb-xs"> + <div class="col vertical-bottom"> + <span class="vertical-middle">{{ $t('faxSettings.secretKeyRenewNotify') }}:</span> + </div> + <div class="col text-right"> + <q-btn + flat + color="primary" + icon="add" + :disable="!dataLoaded || showAddNewRenewEmail" + @click="openAddNewRenewEmail" + > + {{ $t('faxSettings.addEmail') }} + </q-btn> + </div> + </div> + <q-separator /> + <div class="col relative-position"> + <div + v-if="showAddNewRenewEmail" + class="row justify-center q-pa-md" + > + <csc-mail-to-fax-renew-notify-email-form + v-if="showAddNewRenewEmail" + ref="addNewRenewEmailForm" + class="col" + :loading="!dataLoaded" + :is-add-new-mode="true" + @save="addNewRenewEmail" + @cancel="closeAddNewRenewEmail" + /> + </div> + <div + v-if="!showAddNewRenewEmail && (!mailToFaxSettingsModel.secret_renew_notify || !mailToFaxSettingsModel.secret_renew_notify.length)" + class="row q-pa-md justify-center" + > + {{ $t('There are no Key Renew Notify Emails yet') }} + </div> + <div + v-else + class="row q-pa-xs" + > + <q-list class="col striped-list"> + <csc-mail-to-fax-renew-notify-email + v-for="renewEmail in mailToFaxSettingsModel.secret_renew_notify" + :key="renewEmail.destination" + :value="renewEmail.destination" + @save="updateRenewEmailItem(renewEmail.destination, ...arguments)" + @remove="deleteRenewEmailItem(renewEmail.destination)" + /> + </q-list> + </div> + + <q-inner-loading :showing="!dataLoaded"> + <q-spinner-dots + size="50px" + color="primary" + /> + </q-inner-loading> + </div> + </div> + <div class="col q-pa-md"> + <div class="row q-pb-xs"> + <div class="col"> + {{ $t('faxSettings.ACL') }}: + </div> + <div class="col text-right"> + <q-btn + flat + color="primary" + icon="add" + :disable="!dataLoaded || showAddNewACL" + @click="openAddNewACL" + > + {{ $t('faxSettings.addACL') }} + </q-btn> + </div> + </div> + <q-separator /> + <div class="col relative-position"> + <div + v-if="showAddNewACL" + class="row justify-center q-pa-md" + > + <csc-mail-to-fax-a-c-l-form + v-if="showAddNewACL" + ref="addNewACLForm" + class="col" + :loading="!dataLoaded" + :is-add-new-mode="true" + @save="addNewACL" + @cancel="closeAddNewACL" + /> + </div> + <div + v-if="!showAddNewACL && (!mailToFaxSettingsModel.acl || !mailToFaxSettingsModel.acl.length)" + class="row q-pa-md justify-center" + > + {{ $t('There are no ACLs yet') }} + </div> + <div + v-else + class="row q-pa-xs" + > + <q-list class="col striped-list"> + <csc-mail-to-fax-a-c-l + v-for="(acl, index) in mailToFaxSettingsModel.acl" + :key="index" + :acl="acl" + :expanded="index === expandedACLId" + @expand="expandedACLId = index" + @collapse="expandedACLId = null" + @update-property="updateACL(index, ...arguments)" + @remove="deleteACL(index)" + /> + </q-list> + </div> + + <q-inner-loading :showing="!dataLoaded"> + <q-spinner-dots + size="50px" + color="primary" + /> + </q-inner-loading> + </div> + </div> + </div> + </div> +</template> + +<script> +import _ from 'lodash' +import { mapState } from 'vuex' +import { mapWaitingActions, mapWaitingGetters } from 'vue-wait' +import { showGlobalError } from 'src/helpers/ui' +import CscSpinner from 'components/CscSpinner' +import CscInputSaveable from 'components/form/CscInputSaveable' +import CscMailToFaxRenewNotifyEmail from 'components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail' +import CscMailToFaxACL from 'components/pages/FaxSettings/CscMailToFaxACL' +import CscMailToFaxRenewNotifyEmailForm from 'components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm' +import CscMailToFaxACLForm from 'components/pages/FaxSettings/CscMailToFaxACLForm' +import CscTooltip from 'components/CscTooltip' + +export default { + name: 'CscMailToFaxSettings', + components: { + CscTooltip, + CscMailToFaxACLForm, + CscMailToFaxRenewNotifyEmailForm, + CscMailToFaxACL, + CscMailToFaxRenewNotifyEmail, + CscInputSaveable, + CscSpinner + }, + data () { + return { + mailToFaxSettingsModel: {}, + showAddNewRenewEmail: false, + showAddNewACL: false, + expandedACLId: null + } + }, + computed: { + ...mapState('fax', [ + 'mailToFaxSettings', + 'mailToFaxSettingsInitialized' + ]), + ...mapWaitingGetters({ + loadingMail2FaxSettings: 'loading mail2faxSettings' + }), + dataLoaded () { + return this.mailToFaxSettingsInitialized && !this.loadingMail2FaxSettings + }, + secretKeyFieldLabel () { + let label = this.$t('faxSettings.secretKeyField') + label += ' (' + this.$t('faxSettings.lastModifyTime') + ': ' + if (this.mailToFaxSettings.last_secret_key_modify) { + label += this.mailToFaxSettings.last_secret_key_modify + ')' + } else { + label += this.$t('faxSettings.notModifiedYet') + ')' + } + return label + }, + secretKeyRenewOptions () { + return [ + { value: 'never', label: this.$t('Never') }, + { value: 'daily', label: this.$t('Daily') }, + { value: 'weekly', label: this.$t('Weekly') }, + { value: 'monthly', label: this.$t('Monthly') } + ] + } + }, + mounted () { + this.loadMailToFaxSettings() + }, + methods: { + ...mapWaitingActions('fax', { + loadMailToFaxSettingsAction: 'loading mail2faxSettings', + mailToFaxSettingsUpdateAction: 'loading mail2faxSettings' + }), + async loadMailToFaxSettings () { + try { + await this.loadMailToFaxSettingsAction() + this.updateDataFromStore() + } catch (err) { + if (String(err.code) === '403') { + this.mailToFaxSettingsModel = { + active: false + } + } else { + showGlobalError(err?.message || this.$t('Unknown error')) + } + } + }, + updateDataFromStore () { + this.mailToFaxSettingsModel = { + active: true, + ..._.cloneDeep(this.mailToFaxSettings) + } + }, + async setChangedData (field, value, beforeUpdateUI = () => {}) { + try { + await this.mailToFaxSettingsUpdateAction({ field, value }) + beforeUpdateUI() + this.updateDataFromStore() + } catch (err) { + showGlobalError(err?.message || this.$t('Unknown error')) + } + }, + openAddNewRenewEmail () { + this.showAddNewRenewEmail = true + }, + closeAddNewRenewEmail () { + this.showAddNewRenewEmail = false + this.$refs.addNewRenewEmailForm.reset() + }, + addNewRenewEmail (newItemData) { + const renewEmailItems = [...this.mailToFaxSettingsModel.secret_renew_notify, newItemData] + + this.setChangedData('secret_renew_notify', renewEmailItems, () => { + this.closeAddNewRenewEmail() + }) + }, + updateRenewEmailItem (itemId, data) { + const renewEmailItems = _.cloneDeep(this.mailToFaxSettingsModel.secret_renew_notify) + const renewEmailItemIndex = renewEmailItems.findIndex(d => d.destination === itemId) + if (renewEmailItemIndex >= 0) { + renewEmailItems[renewEmailItemIndex].destination = data.value + } + + this.setChangedData('secret_renew_notify', renewEmailItems) + }, + deleteRenewEmailItem (itemId) { + const renewEmailItems = this.mailToFaxSettingsModel.secret_renew_notify.filter(d => d.destination !== itemId) + this.setChangedData('secret_renew_notify', renewEmailItems) + }, + + openAddNewACL () { + this.showAddNewACL = true + }, + closeAddNewACL () { + this.showAddNewACL = false + this.$refs.addNewACLForm.reset() + }, + addNewACL (newItemData) { + const ACLItems = [...this.mailToFaxSettingsModel.acl, newItemData] + + this.setChangedData('acl', ACLItems, () => { + this.closeAddNewACL() + }) + }, + updateACL (itemId, { name, value }) { + const ACLItems = _.cloneDeep(this.mailToFaxSettingsModel.acl) + if (itemId >= 0) { + ACLItems[itemId][name] = value + } + + this.setChangedData('acl', ACLItems) + }, + deleteACL (itemId) { + const ACLItems = this.mailToFaxSettingsModel.acl.filter((acl, index) => index !== itemId) + this.setChangedData('acl', ACLItems, () => { + this.expandedACLId = null + }) + } + } +} +</script> + +<style lang="stylus" rel="stylesheet/stylus" scoped> + +</style> diff --git a/src/css/app.styl b/src/css/app.styl index c5823806..f216e03d 100644 --- a/src/css/app.styl +++ b/src/css/app.styl @@ -27,3 +27,9 @@ body.body--dark .csc-opt-center margin-top ($header-height * -2) + +.striped-list + > :nth-of-type(2n+1) + background-color $item-stripe-color + > :nth-of-type(2n) + background-color alpha($main-menu-background, 0.2) diff --git a/src/i18n/en.json b/src/i18n/en.json index 6643c61c..a8e424f2 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -764,7 +764,20 @@ "deleteDestinationTitle": "Remove Destination", "deleteDestinationText": "You are about to remove destination {destination}", "destinationEmailExists": "The Destination Email is already used", - "destinationItemTitle": "<{destination}> as {filetype}" + "destinationItemTitle": "<{destination}> as {filetype}", + "notifyEmailExists": "The Notify Email is already used", + "featureIsNotActive": "Mail To Fax feature is not active", + "destinations": "Destinations", + "secretKeyField": "Secret Key (empty=disabled)", + "lastModifyTime": "Last Modify Time", + "notModifiedYet": "Not modified yet", + "secretKeyRenew": "Secret Key Renew", + "secretKeyRenewNotify": "Secret Key Renew Notify", + "addEmail": "Add email", + "deleteRenewNotifyEmailTitle": "Remove secret key renew notify email", + "deleteRenewNotifyEmailText": "You are about to remove secret key renew notify email: {email}", + "ACL": "ACL", + "addACL": "Add ACL" }, "callSettings": { "musicOnHold": "Music on Hold", diff --git a/src/layouts/CscLayoutMain.vue b/src/layouts/CscLayoutMain.vue index c419b2e1..6eeca715 100644 --- a/src/layouts/CscLayoutMain.vue +++ b/src/layouts/CscLayoutMain.vue @@ -21,7 +21,7 @@ @click="$refs.mainMenu.show()" /> <q-btn - v-if="hasFaxCapability && hasSendFaxFeature" + v-if="hasFaxCapabilityAndFaxActive && hasSendFaxFeature" class="q-mr-sm" flat dense @@ -297,7 +297,7 @@ export default { 'getUsername', 'isPbxAdmin', 'hasSmsCapability', - 'hasFaxCapability', + 'hasFaxCapabilityAndFaxActive', 'hasSendSmsFeature', 'hasSendFaxFeature', 'userDataRequesting', @@ -315,7 +315,7 @@ export default { ]), hasCommunicationCapabilities () { return (this.hasSmsCapability && this.hasSendSmsFeature) || - (this.hasFaxCapability && this.hasSendFaxFeature) + (this.hasFaxCapabilityAndFaxActive && this.hasSendFaxFeature) }, isMenuClosed () { return !this.sideStates.left diff --git a/src/pages/CscPageFaxSettings.vue b/src/pages/CscPageFaxSettings.vue index 5220e3fc..bc3a8bab 100644 --- a/src/pages/CscPageFaxSettings.vue +++ b/src/pages/CscPageFaxSettings.vue @@ -1,275 +1,46 @@ <template> - <csc-page - class="q-pa-lg" + <csc-page-sticky-tabs + v-model="selectedTab" > - <q-list - class="col col-xs-12 col-md-6" - dense + <template + v-slot:tabs > - <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> + <q-tab + name="fax2mail" + icon="perm_phone_msg" + :label="$t('Fax to Mail and Sendfax')" + /> + <q-tab + name="mail2fax" + icon="forward_to_inbox" + :label="$t('Mail to Fax')" + /> + </template> + + <csc-fax-to-mail-settings + v-if="selectedTab === 'fax2mail'" + /> + <csc-mail-to-fax-settings + v-if="selectedTab === 'mail2fax'" + /> + </csc-page-sticky-tabs> </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' +import CscPageStickyTabs from 'components/CscPageStickyTabs' +import CscFaxToMailSettings from 'components/pages/FaxSettings/CscFaxToMailSettings' +import CscMailToFaxSettings from 'components/pages/FaxSettings/CscMailToFaxSettings' + export default { name: 'CscPageFaxSettings', components: { - CscFax2MailDestination, - CscFax2MailDestinationForm, - CscSpinner, - CscPage, - CscInputSaveable + CscPageStickyTabs, + CscMailToFaxSettings, + CscFaxToMailSettings }, 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) + selectedTab: 'fax2mail' } } } diff --git a/src/plugins/rtc-engine.js b/src/plugins/rtc-engine.js index 9579d71d..b0b93b3a 100644 --- a/src/plugins/rtc-engine.js +++ b/src/plugins/rtc-engine.js @@ -107,18 +107,22 @@ export class RtcEnginePlugin { }) this.client.onConnect(() => { this.events.emit('connected') - const conferenceNetwork = this.client.getNetworkByTag('conference') - conferenceNetwork.onConnect(() => { - this.events.emit('conference-network-connected', conferenceNetwork) - }).onDisconnect(() => { - this.events.emit('conference-network-disconnected', conferenceNetwork) - }) - const sipNetwork = this.client.getNetworkByTag('sip') - sipNetwork.onConnect(() => { - this.events.emit('sip-network-connected', sipNetwork) - }).onDisconnect(() => { - this.events.emit('sip-network-disconnected', sipNetwork) - }) + try { + const conferenceNetwork = this.client.getNetworkByTag('conference') + conferenceNetwork.onConnect(() => { + this.events.emit('conference-network-connected', conferenceNetwork) + }).onDisconnect(() => { + this.events.emit('conference-network-disconnected', conferenceNetwork) + }) + const sipNetwork = this.client.getNetworkByTag('sip') + sipNetwork.onConnect(() => { + this.events.emit('sip-network-connected', sipNetwork) + }).onDisconnect(() => { + this.events.emit('sip-network-disconnected', sipNetwork) + }) + } catch (e) { + reject(new Error('Unable to connect to a specific network by RTCEngine client')) + } resolve() }) this.client.onDisconnect(() => { diff --git a/src/router/routes.js b/src/router/routes.js index 943561c0..239f0301 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -195,6 +195,14 @@ export default function routes (app) { meta: { title: i18n.t('navigation.faxSettings.title'), subtitle: i18n.t('navigation.faxSettings.subTitle') + }, + async beforeEnter (routeTo, routeFrom, next) { + await app.store.dispatch('user/initUser') + if (app.store.getters['user/hasFaxCapability']) { + next() + } else { + next('/') + } } }, { diff --git a/src/store/call-forward.js b/src/store/call-forward.js index f19b8f1d..22ea193c 100644 --- a/src/store/call-forward.js +++ b/src/store/call-forward.js @@ -99,8 +99,8 @@ export default { updateOwnPhoneTimeoutError: null }, getters: { - hasFaxCapability (state, getters, rootState, rootGetters) { - return rootGetters['user/hasFaxCapability'] + hasFaxCapabilityAndFaxActive (state, getters, rootState, rootGetters) { + return rootGetters['user/hasFaxCapabilityAndFaxActive'] }, subscriberId (state, getters, rootState, rootGetters) { return rootGetters['user/getSubscriberId'] diff --git a/src/store/fax.js b/src/store/fax.js index cce527d5..567a83e6 100644 --- a/src/store/fax.js +++ b/src/store/fax.js @@ -1,14 +1,19 @@ import _ from 'lodash' import { getFaxServerSettings, - setFaxServerField + setFaxServerField, + getMailToFaxSettings, + setMailToFaxSettingField } from '../api/fax' export default { namespaced: true, state: { faxServerSettingsInitialized: false, - faxServerSettings: {} + faxServerSettings: {}, + + mailToFaxSettingsInitialized: false, + mailToFaxSettings: {} }, getters: { subscriberId (state, getters, rootState, rootGetters) { @@ -21,6 +26,11 @@ export default { state.faxServerSettings = res.faxServerSettings state.faxServerSettingsInitialized = true } + + if (_.has(res, 'mailToFaxSettings')) { + state.mailToFaxSettings = res.mailToFaxSettings + state.mailToFaxSettingsInitialized = true + } } }, actions: { @@ -30,7 +40,7 @@ export default { faxServerSettings }) }, - async fieldUpdateAction (context, options) { + async faxServerSettingsUpdateAction (context, options) { const faxServerSettings = await setFaxServerField({ subscriberId: context.getters.subscriberId, field: options.field, @@ -40,6 +50,23 @@ export default { faxServerSettings }) context.commit('user/updateFaxActiveCapabilityState', faxServerSettings.active, { root: true }) + }, + + async loadMailToFaxSettingsAction (context) { + const mailToFaxSettings = await getMailToFaxSettings(context.getters.subscriberId) + context.commit('settingsSucceeded', { + mailToFaxSettings + }) + }, + async mailToFaxSettingsUpdateAction (context, options) { + const mailToFaxSettings = await setMailToFaxSettingField({ + subscriberId: context.getters.subscriberId, + field: options.field, + value: options.value + }) + context.commit('settingsSucceeded', { + mailToFaxSettings + }) } } } diff --git a/src/store/user.js b/src/store/user.js index 92fe9b11..e949b989 100644 --- a/src/store/user.js +++ b/src/store/user.js @@ -83,6 +83,10 @@ export default { return state.features.sendFax }, hasFaxCapability (state) { + return state.capabilities !== null && + state.capabilities.faxserver + }, + hasFaxCapabilityAndFaxActive (state) { return state.capabilities !== null && state.capabilities.faxserver && state.capabilities.faxactive @@ -360,7 +364,7 @@ export default { } }, async forwardHome (context) { - if (context.rootState.route.path === '/user/home' && !context.getters.isRtcEngineUiVisible) { + if (context.rootState.route?.path === '/user/home' && !context.getters.isRtcEngineUiVisible) { await router.push({ path: '/user/conversations' }) } },