fix: поиск сотрудника в маршруте без учёта регистра

Фильтр SharePoint substringof по Title пользователей сайта для кириллицы
чувствителен к регистру: «тимур» не находил «Тимур Городецкий». Теперь
ищутся сразу несколько написаний (как введено, строчными, с заглавной
каждого слова) и по email. Касается всех полей выбора сотрудника в
маршруте: делегирование, добавление согласующего, замена исполнителя.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-28 16:29:29 +05:00
parent 247f614885
commit 1b1a783c98
3 changed files with 56 additions and 3 deletions
@@ -11,6 +11,7 @@ import { IApprovalRow } from './models/IApprovalRow';
import { IMatrixRow } from './models/IMatrixRow'; import { IMatrixRow } from './models/IMatrixRow';
import { ISiteUser } from './models/ISiteUser'; import { ISiteUser } from './models/ISiteUser';
import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses'; import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
import { userSearchVariants } from './userSearchVariants';
// Внутренние имена колонок списка согласования (создаются провижинингом). // Внутренние имена колонок списка согласования (создаются провижинингом).
const FIELDS = { const FIELDS = {
@@ -336,13 +337,18 @@ export class ApprovalListService {
}); });
} }
// Поиск пользователей сайта по части имени — для пикера согласующего. // Поиск пользователей сайта по части имени или почты — для пикера согласующего.
// Несколько написаний сразу: substringof по Title для кириллицы чувствителен к
// регистру (см. userSearchVariants). Почта в SharePoint хранится строчными —
// по ней достаточно строчного варианта.
public async searchUsers(query: string): Promise<ISiteUser[]> { public async searchUsers(query: string): Promise<ISiteUser[]> {
const q = query.trim(); const q = query.trim();
if (q.length < 2) return []; 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() 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') .select('Id', 'Title', 'Email', 'LoginName')
.top(10)()) as Array<Record<string, unknown>>; .top(10)()) as Array<Record<string, unknown>>;
return users.map((u) => ({ return users.map((u) => ({
@@ -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([]);
});
});
@@ -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;
}