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

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

@ -1,3 +1,5 @@
import { sortDestinationsByPriority } from 'src/helpers/call-forwarding-destinations'
export function dataSucceeded (state, res) {
if (res.bNumberSets) {
const bNumberSetMap = {}
@ -9,11 +11,17 @@ export function dataSucceeded (state, res) {
}
if (res.destinationSets) {
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
})
state.destinationSetMap = destinationSetMap
state.destinationSets = res.destinationSets
state.destinationSets = orderedDestinationSets
}
if (res.sourceSets) {
const sourceSetMap = {}

@ -28,14 +28,13 @@ describe('Destination Helpers', () => {
expect(DestinationType.AutoAttendant).toBe('AutoAttendant')
expect(DestinationType.OfficeHoursAnnouncement).toBe('OfficeHoursAnnouncement')
expect(DestinationType.CustomAnnouncement).toBe('CustomAnnouncement')
expect(DestinationType.LocalSubscriber).toBe('LocalSubscriber')
expect(DestinationType.ManagerSecretary).toBe('ManagerSecretary')
expect(DestinationType.Application).toBe('Application')
expect(DestinationType.Number).toBe('Number')
})
it('should have 12 destination types', () => {
expect(Object.keys(DestinationType)).toHaveLength(12)
it('should have 11 destination types', () => {
expect(Object.keys(DestinationType)).toHaveLength(11)
})
})
@ -90,13 +89,6 @@ describe('Destination Helpers', () => {
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', () => {
const result = parseSipUri('sip:ms@managersecretary.local')
expect(result.destinationType).toBe(DestinationType.ManagerSecretary)
@ -185,11 +177,6 @@ describe('Destination Helpers', () => {
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', () => {
expect(isDestinationTypeManagerSecretary('sip:ms@managersecretary.local')).toBe(true)
expect(isDestinationTypeManagerSecretary('sip:ms@example.com')).toBe(false)
@ -239,10 +226,6 @@ describe('Destination Helpers', () => {
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', () => {
expect(getDestinationIcon(DestinationType.ManagerSecretary)).toBe('support_agent')
})
@ -303,7 +286,6 @@ describe('Destination Helpers', () => {
'sip:auto-attendant@app.local',
'sip:office-hours@app.local',
'sip:custom-hours@app.local',
'sip:localuser@local',
'sip:ms@managersecretary.local',
'sip:anyapp@app.local',
'sip:1234567890@example.com'
@ -318,7 +300,6 @@ describe('Destination Helpers', () => {
DestinationType.AutoAttendant,
DestinationType.OfficeHoursAnnouncement,
DestinationType.CustomAnnouncement,
DestinationType.LocalSubscriber,
DestinationType.ManagerSecretary,
DestinationType.Application,
DestinationType.Number

Loading…
Cancel
Save