diff --git a/src/helpers/call-forwarding-destinations.js b/src/helpers/call-forwarding-destinations.js new file mode 100644 index 00000000..53156137 --- /dev/null +++ b/src/helpers/call-forwarding-destinations.js @@ -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 +} diff --git a/src/helpers/destination.js b/src/helpers/destination.js index 500b99b5..3d705c9c 100644 --- a/src/helpers/destination.js +++ b/src/helpers/destination.js @@ -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' diff --git a/src/store/call-forwarding/actions.js b/src/store/call-forwarding/actions.js index 24e121eb..99ad12c7 100644 --- a/src/store/call-forwarding/actions.js +++ b/src/store/call-forwarding/actions.js @@ -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) diff --git a/src/store/call-forwarding/mutations.js b/src/store/call-forwarding/mutations.js index 2894cb55..fffbcd81 100644 --- a/src/store/call-forwarding/mutations.js +++ b/src/store/call-forwarding/mutations.js @@ -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 = {} diff --git a/test/jest/__tests__/helpers/destination.spec.js b/test/jest/__tests__/helpers/destination.spec.js index 3a0b9005..20ff02fd 100644 --- a/test/jest/__tests__/helpers/destination.spec.js +++ b/test/jest/__tests__/helpers/destination.spec.js @@ -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