From acc4202e8831e38132d4c1fd61cf1c7ad898279c Mon Sep 17 00:00:00 2001 From: Roman Zaritskyi Date: Fri, 11 Sep 2026 11:53:29 +0300 Subject: [PATCH 1/3] feature: add validation of destination filed depends on comm type in popup[WTEL-10374](https://webitel.atlassian.net/browse/WTEL-10374) --- src/app/utils/validators.js | 3 +- ...pened-queue-member-communication-popup.vue | 36 +++++++++++-- .../__tests__/useNormalizeCsvMembers.spec.ts | 53 +++++++++++++++++++ 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/app/utils/validators.js b/src/app/utils/validators.js index a760c1c56..23e072e1a 100644 --- a/src/app/utils/validators.js +++ b/src/app/utils/validators.js @@ -1,4 +1,5 @@ import { helpers } from '@vuelidate/validators'; +import { phoneNumberPattern } from '@webitel/api-services/validations'; export const macValidator = (value) => { if (typeof value === 'undefined' || value === null || value === '') { @@ -46,7 +47,7 @@ export const phoneNumberSymbolsValidator = (value) => { if (typeof value === 'undefined' || value === null || value === '') { return true; } - return /^\+?[A-Za-z0-9\-_.!~*'()]+$/.test(value); + return phoneNumberPattern.test(value); }; export const sipPasswordSymbolsValidator = (value) => { diff --git a/src/modules/contact-center/modules/queues/modules/members/components/communications/opened-queue-member-communication-popup.vue b/src/modules/contact-center/modules/queues/modules/members/components/communications/opened-queue-member-communication-popup.vue index ed7658853..bab571c67 100644 --- a/src/modules/contact-center/modules/queues/modules/members/components/communications/opened-queue-member-communication-popup.vue +++ b/src/modules/contact-center/modules/queues/modules/members/components/communications/opened-queue-member-communication-popup.vue @@ -77,7 +77,11 @@ import { OutboundResourcesAPI as ResourcesAPI, } from '@webitel/api-services/api'; import type { EngineMemberCommunication } from '@webitel/api-services/gen/models'; -import { memberCommunicationSchema } from '@webitel/api-services/validations'; +import { EngineCommunicationChannels } from '@webitel/api-services/gen/models'; +import { + memberCommunicationSchema, + phoneMemberCommunicationSchema, +} from '@webitel/api-services/validations'; import { WtObject } from '@webitel/ui-sdk/enums'; import { computed, ref, toRaw, watch } from 'vue'; import { useI18n } from 'vue-i18n'; @@ -137,7 +141,17 @@ watch( }, ); -const { r$ } = useRegleSchema(draft, memberCommunicationSchema, { +const channels = ref>({}); + +const schema = computed(() => { + const typeId = draft.value.type?.id; + + return typeId && channels.value[typeId] === EngineCommunicationChannels.Phone + ? phoneMemberCommunicationSchema + : memberCommunicationSchema; +}); + +const { r$ } = useRegleSchema(draft, schema, { autoDirty: true, syncState: { onValidate: true, @@ -164,8 +178,22 @@ const save = async () => { close(); }; -const loadCommunicationTypes = (params: unknown) => - CommunicationsAPI.getLookup(params); +const loadCommunicationTypes = async (params: Record) => { + const response = await CommunicationsAPI.getLookup({ + ...params, + fields: [ + 'id', + 'name', + 'channel', + ], + }); + + for (const { id, channel } of response.items) { + if (id && channel) channels.value[id] = channel; + } + + return response; +}; const loadResources = (params: unknown) => ResourcesAPI.getLookup(params); diff --git a/src/modules/contact-center/modules/queues/modules/members/composables/__tests__/useNormalizeCsvMembers.spec.ts b/src/modules/contact-center/modules/queues/modules/members/composables/__tests__/useNormalizeCsvMembers.spec.ts index 1402a3921..784fde875 100644 --- a/src/modules/contact-center/modules/queues/modules/members/composables/__tests__/useNormalizeCsvMembers.spec.ts +++ b/src/modules/contact-center/modules/queues/modules/members/composables/__tests__/useNormalizeCsvMembers.spec.ts @@ -20,10 +20,12 @@ const communicationTypes = [ { id: '10', code: 'phone', + channel: 'Phone', }, { id: '20', code: 'email', + channel: 'Email', }, ]; @@ -158,6 +160,57 @@ describe('useNormalizeCsvMembers', () => { ).rejects.toThrow(RangeError); }); + it('rejects a dialed destination holding symbols a number cannot use', async () => { + const { normalizeData } = setup(); + + await expect( + normalizeData([ + row({ + destination: [ + '380 00 1', + ], + code: [ + 'phone', + ], + }), + ]), + ).rejects.toThrow(SyntaxError); + }); + + it('accepts a dialed destination made of the allowed symbols', async () => { + const { normalizeData } = setup(); + + const [member] = await normalizeData([ + row({ + destination: [ + "+38(000)-1_2.3!4~5*6'7", + ], + code: [ + 'phone', + ], + }), + ]); + + expect(member.communications[0].destination).toBe("+38(000)-1_2.3!4~5*6'7"); + }); + + it('leaves a destination on a channel that is not dialed alone', async () => { + const { normalizeData } = setup(); + + const [member] = await normalizeData([ + row({ + destination: [ + 'joe@example.dev', + ], + code: [ + 'email', + ], + }), + ]); + + expect(member.communications[0].destination).toBe('joe@example.dev'); + }); + it('rejects dtmf that is not digits or w', async () => { const { normalizeData } = setup(); From a5daf9c22232d38f8b3cacbae2fcdfada8b90f39 Mon Sep 17 00:00:00 2001 From: Roman Zaritskyi Date: Fri, 11 Sep 2026 12:08:37 +0300 Subject: [PATCH 2/3] feature: add phone validation to csv import (https://webitel.atlassian.net/browse/WTEL-10374)[WTEL-10374] --- .../composables/useNormalizeCsvMembers.ts | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/modules/contact-center/modules/queues/modules/members/composables/useNormalizeCsvMembers.ts b/src/modules/contact-center/modules/queues/modules/members/composables/useNormalizeCsvMembers.ts index 4e5f6eb3b..c1f4a83f1 100644 --- a/src/modules/contact-center/modules/queues/modules/members/composables/useNormalizeCsvMembers.ts +++ b/src/modules/contact-center/modules/queues/modules/members/composables/useNormalizeCsvMembers.ts @@ -1,8 +1,15 @@ import { CommunicationsAPI, QueueMembersAPI } from '@webitel/api-services/api'; +import { EngineCommunicationChannels } from '@webitel/api-services/gen/models'; +import { + memberCommunicationSchema, + phoneNumberSchema, +} from '@webitel/api-services/validations'; import type { Ref } from 'vue'; -/** carried over verbatim; see memberCommunicationSchema for the same pattern */ -const dtmfPattern = /^[\d|w|W]*$/; +const { dtmf: dtmfSchema } = memberCommunicationSchema.shape; +const destinationSchemaByChannel = { + [EngineCommunicationChannels.Phone]: phoneNumberSchema, +} as const; interface MappingField { name: string; @@ -13,11 +20,8 @@ interface MappingField { // biome-ignore lint/suspicious/noExplicitAny: rows come from a user-supplied csv type CsvRow = Record; -const findCommunicationIdByCode = ( - communications: CsvRow[], - code: string, -): string | undefined => - communications.find((communication) => communication.code === code)?.id; +const findCommunicationByCode = (communications: CsvRow[], code: string) => + communications.find((communication) => communication.code === code); /** * Turns parsed csv rows into queue members. @@ -40,6 +44,12 @@ export const useNormalizeCsvMembers = ({ const normalizeData = async (data: CsvRow[]) => { const { items: allCommunications } = await CommunicationsAPI.getList({ size: 5000, + // `channel` decides which rule a destination is held to + fields: [ + 'id', + 'code', + 'channel', + ], }); return data.map((item) => { @@ -93,19 +103,33 @@ export const useNormalizeCsvMembers = ({ ); for (let index = 0; index < communicationCount; index += 1) { - const id = findCommunicationIdByCode( + const type = findCommunicationByCode( allCommunications, normalized.code[index], ); - if (!id) { + if (!type) { console.error(`cannot find communication: ${normalized.code[index]}`); } + const id = type?.id; + const destination = normalized.destination[index]; + // a communication needs both a type and somewhere to reach - if (!id || !normalized.destination[index]) continue; + if (!id || !destination) continue; + + const destinationSchema = + destinationSchemaByChannel[ + type.channel as keyof typeof destinationSchemaByChannel + ]; + const checked = destinationSchema?.safeParse(destination); + + if (checked && !checked.success) { + // already localized: `configureZod` installs a global error map + throw new SyntaxError(checked.error.issues[0].message); + } const communication: CsvRow = { - destination: normalized.destination[index], + destination, type: { id, }, @@ -118,7 +142,7 @@ export const useNormalizeCsvMembers = ({ communication.description = normalized.description[index]; } if (normalized.dtmf?.[index]) { - if (!dtmfPattern.test(normalized.dtmf[index])) { + if (!dtmfSchema.safeParse(normalized.dtmf[index]).success) { throw new SyntaxError('No valid DTMF were passed!'); } communication.dtmf = normalized.dtmf[index]; From a7644a404253b68ab753f604dd03238ebdc9e936 Mon Sep 17 00:00:00 2001 From: Roman Zaritskyi Date: Thu, 17 Sep 2026 10:04:09 +0300 Subject: [PATCH 3/3] chore: update api-services [WTEL-10374](https://webitel.atlassian.net/browse/WTEL-10374) --- package-lock.json | 6 +++--- src/app/utils/validators.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2547d0dc..5013c5d5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3991,9 +3991,9 @@ } }, "node_modules/@webitel/api-services": { - "version": "26.8.28", - "resolved": "https://registry.npmjs.org/@webitel/api-services/-/api-services-26.8.28.tgz", - "integrity": "sha512-vfYE3YVPXrasR+psS28U7tVv0SIqeG/sKtgCOdbGQIoPi9JxhBR8D0pjtuq17dIOV7UZoWFyvkg3Emi7fwwRiA==", + "version": "26.8.31", + "resolved": "https://registry.npmjs.org/@webitel/api-services/-/api-services-26.8.31.tgz", + "integrity": "sha512-4+CZU4TJekNP9VXS6z4J95gyYH2zICqjn/gAKS7VnSHGyZY44wD8BXb9ID3OjAw/iTG6dG6SKIyusQrRZll3BA==", "dependencies": { "date-fns": "^4.1.0", "lodash-es": "^4.17.21", diff --git a/src/app/utils/validators.js b/src/app/utils/validators.js index 23e072e1a..a203927e1 100644 --- a/src/app/utils/validators.js +++ b/src/app/utils/validators.js @@ -1,5 +1,5 @@ import { helpers } from '@vuelidate/validators'; -import { phoneNumberPattern } from '@webitel/api-services/validations'; +import { phoneNumberSchema } from '@webitel/api-services/validations'; export const macValidator = (value) => { if (typeof value === 'undefined' || value === null || value === '') { @@ -47,7 +47,7 @@ export const phoneNumberSymbolsValidator = (value) => { if (typeof value === 'undefined' || value === null || value === '') { return true; } - return phoneNumberPattern.test(value); + return phoneNumberSchema.safeParse(value).success; }; export const sipPasswordSymbolsValidator = (value) => {