diff --git a/src/webparts/requestPortal/shared/approval/approvalList.service.ts b/src/webparts/requestPortal/shared/approval/approvalList.service.ts index 537d236..e75b2ee 100644 --- a/src/webparts/requestPortal/shared/approval/approvalList.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalList.service.ts @@ -11,6 +11,7 @@ import { IApprovalRow } from './models/IApprovalRow'; import { IMatrixRow } from './models/IMatrixRow'; import { ISiteUser } from './models/ISiteUser'; import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses'; +import { userSearchVariants } from './userSearchVariants'; // Внутренние имена колонок списка согласования (создаются провижинингом). const FIELDS = { @@ -336,13 +337,18 @@ export class ApprovalListService { }); } - // Поиск пользователей сайта по части имени — для пикера согласующего. + // Поиск пользователей сайта по части имени или почты — для пикера согласующего. + // Несколько написаний сразу: substringof по Title для кириллицы чувствителен к + // регистру (см. userSearchVariants). Почта в SharePoint хранится строчными — + // по ней достаточно строчного варианта. public async searchUsers(query: string): Promise { const q = query.trim(); if (q.length < 2) return []; - const escaped = q.replace(/'/g, "''"); + const escape = (s: string): string => s.replace(/'/g, "''"); + const titleClauses = userSearchVariants(q).map((v) => `substringof('${escape(v)}',Title)`); + const emailClause = `substringof('${escape(q.toLowerCase())}',Email)`; const users = (await getSP() - .web.siteUsers.filter(`substringof('${escaped}',Title) and PrincipalType eq 1`) + .web.siteUsers.filter(`(${titleClauses.concat(emailClause).join(' or ')}) and PrincipalType eq 1`) .select('Id', 'Title', 'Email', 'LoginName') .top(10)()) as Array>; return users.map((u) => ({ diff --git a/src/webparts/requestPortal/shared/approval/userSearchVariants.test.ts b/src/webparts/requestPortal/shared/approval/userSearchVariants.test.ts new file mode 100644 index 0000000..664f472 --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/userSearchVariants.test.ts @@ -0,0 +1,23 @@ +import { userSearchVariants } from './userSearchVariants'; + +describe('userSearchVariants', () => { + it('строчный ввод даёт и вариант с заглавной — так ФИО хранятся в SharePoint', () => { + expect(userSearchVariants('тимур')).toEqual(['тимур', 'Тимур']); + }); + + it('заглавной делается каждое слово', () => { + expect(userSearchVariants('тимур городецкий')).toContain('Тимур Городецкий'); + }); + + it('ввод капсом даёт и строчный, и «Как В Имени»', () => { + expect(userSearchVariants('ТИМУР')).toEqual(['ТИМУР', 'тимур', 'Тимур']); + }); + + it('без повторов, если ввод уже в нужном виде', () => { + expect(userSearchVariants('Тимур')).toEqual(['Тимур', 'тимур']); + }); + + it('пустой ввод — пусто', () => { + expect(userSearchVariants(' ')).toEqual([]); + }); +}); diff --git a/src/webparts/requestPortal/shared/approval/userSearchVariants.ts b/src/webparts/requestPortal/shared/approval/userSearchVariants.ts new file mode 100644 index 0000000..673aa86 --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/userSearchVariants.ts @@ -0,0 +1,24 @@ +/** + * Варианты написания запроса для поиска сотрудника (ApprovalListService.searchUsers). + * + * Фильтр SharePoint `substringof(…, Title)` по пользователям сайта для + * кириллицы чувствителен к регистру: «тимур» не находит «Тимур Городецкий». + * Поэтому ищем сразу несколько написаний — как введено, строчными и «Как В + * Имени» (с заглавной каждое слово, так ФИО и хранятся в Title). Повторы + * убираются, чтобы не раздувать фильтр. + */ +export function userSearchVariants(query: string): string[] { + const typed = query.trim(); + if (!typed) return []; + const lower = typed.toLowerCase(); + const capitalized = lower + .split(/\s+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + const variants: string[] = []; + [typed, lower, capitalized].forEach((v) => { + if (variants.indexOf(v) < 0) variants.push(v); + }); + return variants; +}