From 59b6d1734cb9675cbf308139e6ed2359a00c82a1 Mon Sep 17 00:00:00 2001 From: Giancarlo Errigo Mattos Date: Wed, 4 Mar 2026 16:26:13 +0100 Subject: [PATCH] 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 (cherry picked from commit 38d6e2089c3b9bc5b5d8b136c32f56744b49bbec) (cherry picked from commit f0d78c695f5bf9a349cd3fe3d856829d5a675443) --- src/helpers/call-forwarding-destinations.js | 135 ++++++++++++++++++++ src/mixins/destination.js | 53 +------- src/store/call-forwarding/actions.js | 32 +++-- src/store/call-forwarding/mutations.js | 12 +- 4 files changed, 168 insertions(+), 64 deletions(-) create mode 100644 src/helpers/call-forwarding-destinations.js diff --git a/src/helpers/call-forwarding-destinations.js b/src/helpers/call-forwarding-destinations.js new file mode 100644 index 00000000..052ff31c --- /dev/null +++ b/src/helpers/call-forwarding-destinations.js @@ -0,0 +1,135 @@ +import sipUriParse from 'src/sip-uri-parse' + +export const DestinationType = { + VoiceBox: 'VoiceBox', + Conference: 'Conference', + Fax2Mail: 'Fax2Mail', + CallingCard: 'CallingCard', + CallThrough: 'CallThrough', + AutoAttendant: 'AutoAttendant', + OfficeHoursAnnouncement: 'OfficeHoursAnnouncement', + CustomAnnouncement: 'CustomAnnouncement', + LocalSubscriber: 'LocalSubscriber', + ManagerSecretary: 'ManagerSecretary', + Application: 'Application', + Number: 'Number' +} + +export function parseSipUri (sipUri) { + const parsedUri = sipUriParse(sipUri) + const host = parsedUri.host + const username = parsedUri.username + let destinationType + if (host.endsWith('voicebox.local')) { + destinationType = DestinationType.VoiceBox + } else if (host.endsWith('conference.local')) { + destinationType = DestinationType.Conference + } else if (host.endsWith('fax2mail.local')) { + destinationType = DestinationType.Fax2Mail + } else if (username === 'callingcard' && host.endsWith('app.local')) { + destinationType = DestinationType.CallingCard + } else if (username === 'callthrough' && host.endsWith('app.local')) { + destinationType = DestinationType.CallThrough + } else if (username === 'auto-attendant' && host.endsWith('app.local')) { + destinationType = DestinationType.AutoAttendant + } else if (username === 'office-hours' && host.endsWith('app.local')) { + 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')) { + destinationType = DestinationType.Application + } else { + destinationType = DestinationType.Number + } + return { + destinationType, + parsedUri + } +} + +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 +} diff --git a/src/mixins/destination.js b/src/mixins/destination.js index a1d06e7e..40dec967 100644 --- a/src/mixins/destination.js +++ b/src/mixins/destination.js @@ -1,56 +1,5 @@ import _ from 'lodash' -import sipUriParse from 'src/sip-uri-parse' - -const DestinationType = { - VoiceBox: 'VoiceBox', - Conference: 'Conference', - Fax2Mail: 'Fax2Mail', - CallingCard: 'CallingCard', - CallThrough: 'CallThrough', - AutoAttendant: 'AutoAttendant', - OfficeHoursAnnouncement: 'OfficeHoursAnnouncement', - CustomAnnouncement: 'CustomAnnouncement', - LocalSubscriber: 'LocalSubscriber', - ManagerSecretary: 'ManagerSecretary', - Application: 'Application', - Number: 'Number' -} - -function parseSipUri (sipUri) { - const parsedUri = sipUriParse(sipUri) - const host = parsedUri.host - const username = parsedUri.username - let destinationType - if (host.endsWith('voicebox.local')) { - destinationType = DestinationType.VoiceBox - } else if (host.endsWith('conference.local')) { - destinationType = DestinationType.Conference - } else if (host.endsWith('fax2mail.local')) { - destinationType = DestinationType.Fax2Mail - } else if (username === 'callingcard' && host.endsWith('app.local')) { - destinationType = DestinationType.CallingCard - } else if (username === 'callthrough' && host.endsWith('app.local')) { - destinationType = DestinationType.CallThrough - } else if (username === 'auto-attendant' && host.endsWith('app.local')) { - destinationType = DestinationType.AutoAttendant - } else if (username === 'office-hours' && host.endsWith('app.local')) { - 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')) { - destinationType = DestinationType.Application - } else { - destinationType = DestinationType.Number - } - return { - destinationType, - parsedUri - } -} +import { DestinationType, parseSipUri } from 'src/helpers/call-forwarding-destinations' export default { methods: { diff --git a/src/store/call-forwarding/actions.js b/src/store/call-forwarding/actions.js index e658a428..70c7d301 100644 --- a/src/store/call-forwarding/actions.js +++ b/src/store/call-forwarding/actions.js @@ -24,6 +24,7 @@ import { post, put } 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' @@ -31,10 +32,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') { @@ -47,10 +48,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 }) @@ -154,11 +157,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', { @@ -170,12 +174,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', { @@ -209,11 +218,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', { @@ -226,12 +236,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) @@ -628,11 +639,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) diff --git a/src/store/call-forwarding/mutations.js b/src/store/call-forwarding/mutations.js index 3e33d22a..789756a8 100644 --- a/src/store/call-forwarding/mutations.js +++ b/src/store/call-forwarding/mutations.js @@ -1,11 +1,19 @@ +import { sortDestinationsByPriority } from 'src/helpers/call-forwarding-destinations' + 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 = {}