MT#64569 - Destination priority improvements

* Implemented priority flow as sort -> normalize to 0..n-1
    across fetch/create/edit/remove
    * On creation handles priority incrementally
    * On edit, arranges and normalizes (if needed) priority values
  * Enforced the same move rules in store action (moveDestination)
    to block invalid reorders.
  * Removes references to LocalSubscriber
  * Fixes unit tests

Change-Id: Ib64cc30ea75141692085ec38df5c528565389469
mr26.0
Giancarlo Errigo Mattos 3 months ago committed by Giancarlo Mattos
parent e4b533b17a
commit 38d6e2089c

@ -0,0 +1,84 @@
import { DestinationType, parseSipUri } from 'src/helpers/destination'
function toNumericPriority (destination) {
const parsed = Number(destination?.priority)
return Number.isFinite(parsed) ? parsed : 0
}
function isTerminalDestination (destination) {
const destinationType = parseSipUri(destination?.destination || '').destinationType
return destinationType !== DestinationType.Number
}
function allDestinationsSharePriority (destinations) {
if (!Array.isArray(destinations) || destinations.length === 0) {
return true
}
const firstPriority = toNumericPriority(destinations[0])
return destinations.every((destination) => toNumericPriority(destination) === firstPriority)
}
function arePrioritiesSequential (destinations) {
return destinations.every((destination, index) => toNumericPriority(destination) === index)
}
function getPrioritySortedIndexEntries (destinations) {
const entries = destinations.map((destination, index) => ({
destination,
index,
priority: toNumericPriority(destination)
}))
const nonTerminalEntries = []
const terminalEntries = []
entries.forEach((entry) => {
if (isTerminalDestination(entry.destination)) {
terminalEntries.push(entry)
return
}
nonTerminalEntries.push(entry)
})
const sortByPriorityThenIndex = (left, right) => {
if (left.priority !== right.priority) {
return left.priority - right.priority
}
return left.index - right.index
}
nonTerminalEntries.sort(sortByPriorityThenIndex)
terminalEntries.sort(sortByPriorityThenIndex)
return [
...nonTerminalEntries,
...terminalEntries
]
}
export function sortDestinationsByPriority (destinations) {
if (!Array.isArray(destinations) || destinations.length <= 1) {
return Array.isArray(destinations) ? [...destinations] : []
}
if (allDestinationsSharePriority(destinations)) {
return [...destinations]
}
return getPrioritySortedIndexEntries(destinations).map((entry) => entry.destination)
}
export function normalizePriorities (destinations) {
const sortedDestinations = sortDestinationsByPriority(destinations)
let normalizedDestinations
if (!Array.isArray(destinations) || destinations.length === 0) {
normalizedDestinations = []
} else if (arePrioritiesSequential(sortedDestinations)) {
normalizedDestinations = [...sortedDestinations]
} else if (allDestinationsSharePriority(sortedDestinations)) {
normalizedDestinations = sortedDestinations.map((destination, index) => ({
...destination,
priority: index
}))
} else {
normalizedDestinations = sortedDestinations.map((destination, normalizedPriority) => ({
...destination,
priority: normalizedPriority
}))
}
return normalizedDestinations
}

@ -9,7 +9,6 @@ export const DestinationType = {
AutoAttendant: 'AutoAttendant', AutoAttendant: 'AutoAttendant',
OfficeHoursAnnouncement: 'OfficeHoursAnnouncement', OfficeHoursAnnouncement: 'OfficeHoursAnnouncement',
CustomAnnouncement: 'CustomAnnouncement', CustomAnnouncement: 'CustomAnnouncement',
LocalSubscriber: 'LocalSubscriber',
ManagerSecretary: 'ManagerSecretary', ManagerSecretary: 'ManagerSecretary',
Application: 'Application', Application: 'Application',
Number: 'Number' Number: 'Number'
@ -37,8 +36,6 @@ export function parseSipUri (sipUri) {
destinationType = DestinationType.OfficeHoursAnnouncement destinationType = DestinationType.OfficeHoursAnnouncement
} else if (username === 'custom-hours' && host.endsWith('app.local')) { } else if (username === 'custom-hours' && host.endsWith('app.local')) {
destinationType = DestinationType.CustomAnnouncement destinationType = DestinationType.CustomAnnouncement
} else if (username === 'localuser' && host.endsWith('local')) {
destinationType = DestinationType.LocalSubscriber
} else if (host.endsWith('managersecretary.local')) { } else if (host.endsWith('managersecretary.local')) {
destinationType = DestinationType.ManagerSecretary destinationType = DestinationType.ManagerSecretary
} else if (host.endsWith('app.local')) { } else if (host.endsWith('app.local')) {
@ -90,10 +87,6 @@ export function isDestinationTypeCustomAnnouncement (sipUri) {
return isDestinationType(sipUri, DestinationType.CustomAnnouncement) return isDestinationType(sipUri, DestinationType.CustomAnnouncement)
} }
export function isDestinationTypeLocalSubscriber (sipUri) {
return isDestinationType(sipUri, DestinationType.LocalSubscriber)
}
export function isDestinationTypeManagerSecretary (sipUri) { export function isDestinationTypeManagerSecretary (sipUri) {
return isDestinationType(sipUri, DestinationType.ManagerSecretary) return isDestinationType(sipUri, DestinationType.ManagerSecretary)
} }
@ -116,7 +109,6 @@ export function getDestinationIcon (destinationType) {
case DestinationType.AutoAttendant: return 'dialpad' case DestinationType.AutoAttendant: return 'dialpad'
case DestinationType.OfficeHoursAnnouncement: return 'schedule' case DestinationType.OfficeHoursAnnouncement: return 'schedule'
case DestinationType.CustomAnnouncement: return 'music_note' case DestinationType.CustomAnnouncement: return 'music_note'
case DestinationType.LocalSubscriber: return 'person_pin'
case DestinationType.ManagerSecretary: return 'support_agent' case DestinationType.ManagerSecretary: return 'support_agent'
case DestinationType.Application: return 'apps' case DestinationType.Application: return 'apps'
case DestinationType.Number: return 'phone_forwarded' case DestinationType.Number: return 'phone_forwarded'

@ -30,6 +30,7 @@ import {
post post
} from 'src/api/common' } from 'src/api/common'
import { i18n } from 'src/boot/i18n' import { i18n } from 'src/boot/i18n'
import { normalizePriorities } from 'src/helpers/call-forwarding-destinations'
import { showGlobalError, showGlobalWarning } from 'src/helpers/ui' import { showGlobalError, showGlobalWarning } from 'src/helpers/ui'
import { v4 } from 'uuid' import { v4 } from 'uuid'
@ -37,10 +38,10 @@ const DEFAULT_RING_TIMEOUT = 60
const DEFAULT_PRIORITY = 0 const DEFAULT_PRIORITY = 0
const WAIT_IDENTIFIER = 'csc-cf-mappings-full' const WAIT_IDENTIFIER = 'csc-cf-mappings-full'
function createDefaultDestination (destination, defaultAnnouncementId) { function createDefaultDestination (destination, defaultAnnouncementId, priority = DEFAULT_PRIORITY) {
const payload = { const payload = {
destination: destination || ' ', destination: destination || ' ',
priority: DEFAULT_PRIORITY, priority,
timeout: DEFAULT_RING_TIMEOUT timeout: DEFAULT_RING_TIMEOUT
} }
if (destination === 'customhours') { if (destination === 'customhours') {
@ -53,10 +54,12 @@ export async function loadMappingsFull ({ dispatch, commit, rootGetters }, id) {
dispatch('wait/start', WAIT_IDENTIFIER, { root: true }) dispatch('wait/start', WAIT_IDENTIFIER, { root: true })
const subscriberId = id || rootGetters['user/getSubscriberId'] const subscriberId = id || rootGetters['user/getSubscriberId']
const mappingData = await cfLoadMappingsFull(subscriberId) const mappingData = await cfLoadMappingsFull(subscriberId)
const mappings = mappingData[0]
const destinationSets = mappingData[1].items
commit('dataSucceeded', { commit('dataSucceeded', {
mappings: mappingData[0], mappings,
destinationSets: mappingData[1].items, destinationSets,
sourceSets: mappingData[2].items, sourceSets: mappingData[2].items,
timeSets: mappingData[3].items, timeSets: mappingData[3].items,
bNumberSets: mappingData[4].items bNumberSets: mappingData[4].items
@ -179,11 +182,12 @@ export async function updateDestination ({ dispatch, commit, state }, payload) {
dispatch('wait/start', 'csc-cf-destination-set-update', { root: true }) dispatch('wait/start', 'csc-cf-destination-set-update', { root: true })
const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations) const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations)
destinations[payload.destinationIndex].destination = payload.destination destinations[payload.destinationIndex].destination = payload.destination
const normalizedDestinations = normalizePriorities(destinations)
await patchReplace({ await patchReplace({
resource: 'cfdestinationsets', resource: 'cfdestinationsets',
resourceId: payload.destinationSetId, resourceId: payload.destinationSetId,
fieldPath: 'destinations', fieldPath: 'destinations',
value: destinations value: normalizedDestinations
}) })
const destinationSets = await cfLoadDestinationSets() const destinationSets = await cfLoadDestinationSets()
commit('dataSucceeded', { commit('dataSucceeded', {
@ -195,12 +199,17 @@ export async function updateDestination ({ dispatch, commit, state }, payload) {
export async function addDestination ({ dispatch, commit, state, rootGetters }, payload) { export async function addDestination ({ dispatch, commit, state, rootGetters }, payload) {
dispatch('wait/start', WAIT_IDENTIFIER, { root: true }) dispatch('wait/start', WAIT_IDENTIFIER, { root: true })
const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations) const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations)
destinations.push(createDefaultDestination(payload.destination, payload.defaultAnnouncementId)) const normalizedDestinations = normalizePriorities(destinations)
normalizedDestinations.push(createDefaultDestination(
payload.destination,
payload.defaultAnnouncementId,
normalizedDestinations.length
))
await patchReplace({ await patchReplace({
resource: 'cfdestinationsets', resource: 'cfdestinationsets',
resourceId: payload.destinationSetId, resourceId: payload.destinationSetId,
fieldPath: 'destinations', fieldPath: 'destinations',
value: destinations value: normalizedDestinations
}) })
const destinationSets = await cfLoadDestinationSets() const destinationSets = await cfLoadDestinationSets()
commit('dataSucceeded', { commit('dataSucceeded', {
@ -234,11 +243,12 @@ export async function removeDestination ({ dispatch, commit, state }, payload) {
} }
return $updatedDestinations return $updatedDestinations
}, []) }, [])
const normalizedDestinations = normalizePriorities(updatedDestinations)
await patchReplace({ await patchReplace({
resource: 'cfdestinationsets', resource: 'cfdestinationsets',
resourceId: payload.destinationSetId, resourceId: payload.destinationSetId,
fieldPath: 'destinations', fieldPath: 'destinations',
value: updatedDestinations value: normalizedDestinations
}) })
const destinationSets = await cfLoadDestinationSets() const destinationSets = await cfLoadDestinationSets()
commit('dataSucceeded', { commit('dataSucceeded', {
@ -251,12 +261,13 @@ export async function updateDestinationTimeout ({ dispatch, commit, state }, pay
dispatch('wait/start', WAIT_IDENTIFIER, { root: true }) dispatch('wait/start', WAIT_IDENTIFIER, { root: true })
const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations) const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations)
destinations[payload.destinationIndex].timeout = payload.destinationTimeout destinations[payload.destinationIndex].timeout = payload.destinationTimeout
const normalizedDestinations = normalizePriorities(destinations)
try { try {
await patchReplace({ await patchReplace({
resource: 'cfdestinationsets', resource: 'cfdestinationsets',
resourceId: payload.destinationSetId, resourceId: payload.destinationSetId,
fieldPath: 'destinations', fieldPath: 'destinations',
value: destinations value: normalizedDestinations
}) })
} catch (e) { } catch (e) {
showGlobalError(e.message) showGlobalError(e.message)
@ -752,11 +763,12 @@ export async function updateAnnouncement ({ dispatch, commit, state }, payload)
try { try {
const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations) const destinations = _.cloneDeep(state.destinationSetMap[payload.destinationSetId].destinations)
destinations[payload.destinationIndex].announcement_id = payload.announcementId destinations[payload.destinationIndex].announcement_id = payload.announcementId
const normalizedDestinations = normalizePriorities(destinations)
await patchReplace({ await patchReplace({
resource: 'cfdestinationsets', resource: 'cfdestinationsets',
resourceId: payload.destinationSetId, resourceId: payload.destinationSetId,
fieldPath: 'destinations', fieldPath: 'destinations',
value: destinations value: normalizedDestinations
}) })
} catch (e) { } catch (e) {
showGlobalError(e.message) showGlobalError(e.message)

@ -1,3 +1,5 @@
import { sortDestinationsByPriority } from 'src/helpers/call-forwarding-destinations'
export function dataSucceeded (state, res) { export function dataSucceeded (state, res) {
if (res.bNumberSets) { if (res.bNumberSets) {
const bNumberSetMap = {} const bNumberSetMap = {}
@ -9,11 +11,17 @@ export function dataSucceeded (state, res) {
} }
if (res.destinationSets) { if (res.destinationSets) {
const destinationSetMap = {} const destinationSetMap = {}
res.destinationSets.forEach((destinationSet) => { const orderedDestinationSets = res.destinationSets.map((destinationSet) => {
return {
...destinationSet,
destinations: sortDestinationsByPriority(destinationSet.destinations)
}
})
orderedDestinationSets.forEach((destinationSet) => {
destinationSetMap[destinationSet.id] = destinationSet destinationSetMap[destinationSet.id] = destinationSet
}) })
state.destinationSetMap = destinationSetMap state.destinationSetMap = destinationSetMap
state.destinationSets = res.destinationSets state.destinationSets = orderedDestinationSets
} }
if (res.sourceSets) { if (res.sourceSets) {
const sourceSetMap = {} const sourceSetMap = {}

@ -28,14 +28,13 @@ describe('Destination Helpers', () => {
expect(DestinationType.AutoAttendant).toBe('AutoAttendant') expect(DestinationType.AutoAttendant).toBe('AutoAttendant')
expect(DestinationType.OfficeHoursAnnouncement).toBe('OfficeHoursAnnouncement') expect(DestinationType.OfficeHoursAnnouncement).toBe('OfficeHoursAnnouncement')
expect(DestinationType.CustomAnnouncement).toBe('CustomAnnouncement') expect(DestinationType.CustomAnnouncement).toBe('CustomAnnouncement')
expect(DestinationType.LocalSubscriber).toBe('LocalSubscriber')
expect(DestinationType.ManagerSecretary).toBe('ManagerSecretary') expect(DestinationType.ManagerSecretary).toBe('ManagerSecretary')
expect(DestinationType.Application).toBe('Application') expect(DestinationType.Application).toBe('Application')
expect(DestinationType.Number).toBe('Number') expect(DestinationType.Number).toBe('Number')
}) })
it('should have 12 destination types', () => { it('should have 11 destination types', () => {
expect(Object.keys(DestinationType)).toHaveLength(12) expect(Object.keys(DestinationType)).toHaveLength(11)
}) })
}) })
@ -90,13 +89,6 @@ describe('Destination Helpers', () => {
expect(result.parsedUri.username).toBe('custom-hours') expect(result.parsedUri.username).toBe('custom-hours')
}) })
it('should parse LocalSubscriber URI', () => {
const result = parseSipUri('sip:localuser@local')
expect(result.destinationType).toBe(DestinationType.LocalSubscriber)
expect(result.parsedUri.username).toBe('localuser')
expect(result.parsedUri.host).toBe('local')
})
it('should parse ManagerSecretary URI', () => { it('should parse ManagerSecretary URI', () => {
const result = parseSipUri('sip:ms@managersecretary.local') const result = parseSipUri('sip:ms@managersecretary.local')
expect(result.destinationType).toBe(DestinationType.ManagerSecretary) expect(result.destinationType).toBe(DestinationType.ManagerSecretary)
@ -185,11 +177,6 @@ describe('Destination Helpers', () => {
expect(isDestinationTypeCustomAnnouncement('sip:other@app.local')).toBe(false) expect(isDestinationTypeCustomAnnouncement('sip:other@app.local')).toBe(false)
}) })
it('isDestinationTypeLocalSubscriber should work correctly', () => {
expect(isDestinationTypeLocalSubscriber('sip:localuser@local')).toBe(true)
expect(isDestinationTypeLocalSubscriber('sip:user@example.com')).toBe(false)
})
it('isDestinationTypeManagerSecretary should work correctly', () => { it('isDestinationTypeManagerSecretary should work correctly', () => {
expect(isDestinationTypeManagerSecretary('sip:ms@managersecretary.local')).toBe(true) expect(isDestinationTypeManagerSecretary('sip:ms@managersecretary.local')).toBe(true)
expect(isDestinationTypeManagerSecretary('sip:ms@example.com')).toBe(false) expect(isDestinationTypeManagerSecretary('sip:ms@example.com')).toBe(false)
@ -239,10 +226,6 @@ describe('Destination Helpers', () => {
expect(getDestinationIcon(DestinationType.CustomAnnouncement)).toBe('music_note') expect(getDestinationIcon(DestinationType.CustomAnnouncement)).toBe('music_note')
}) })
it('should return correct icon for LocalSubscriber', () => {
expect(getDestinationIcon(DestinationType.LocalSubscriber)).toBe('person_pin')
})
it('should return correct icon for ManagerSecretary', () => { it('should return correct icon for ManagerSecretary', () => {
expect(getDestinationIcon(DestinationType.ManagerSecretary)).toBe('support_agent') expect(getDestinationIcon(DestinationType.ManagerSecretary)).toBe('support_agent')
}) })
@ -303,7 +286,6 @@ describe('Destination Helpers', () => {
'sip:auto-attendant@app.local', 'sip:auto-attendant@app.local',
'sip:office-hours@app.local', 'sip:office-hours@app.local',
'sip:custom-hours@app.local', 'sip:custom-hours@app.local',
'sip:localuser@local',
'sip:ms@managersecretary.local', 'sip:ms@managersecretary.local',
'sip:anyapp@app.local', 'sip:anyapp@app.local',
'sip:1234567890@example.com' 'sip:1234567890@example.com'
@ -318,7 +300,6 @@ describe('Destination Helpers', () => {
DestinationType.AutoAttendant, DestinationType.AutoAttendant,
DestinationType.OfficeHoursAnnouncement, DestinationType.OfficeHoursAnnouncement,
DestinationType.CustomAnnouncement, DestinationType.CustomAnnouncement,
DestinationType.LocalSubscriber,
DestinationType.ManagerSecretary, DestinationType.ManagerSecretary,
DestinationType.Application, DestinationType.Application,
DestinationType.Number DestinationType.Number

Loading…
Cancel
Save