куча доработок
This commit is contained in:
@@ -41,20 +41,18 @@ const MP_APPROVER = 'TansuSiteApproverOWSUSER';
|
||||
// Статус СТРОКИ МАРШРУТА (колонка ApprovalStatus списка «— согласование») —
|
||||
// Choice-поле, отсюда суффикс OWSCHCS (подтверждено отладочным перебором
|
||||
// 2026-08-06; у Person-полей он OWSUSER, см. MP_APPROVER). Колонку не путать с
|
||||
// RequestStatus списка ДАННЫХ — это статус ЗАЯВКИ целиком, у него своя
|
||||
// managed property (см. MP_TICKET_STATUS ниже).
|
||||
// RequestStatus списка ДАННЫХ — это статус ЗАЯВКИ целиком.
|
||||
//
|
||||
// Для статуса ЗАЯВКИ (список ДАННЫХ) отдельной managed property здесь
|
||||
// НАМЕРЕННО нет: имя для произвольной Choice-колонки в схеме поиска либо надо
|
||||
// знать точно, либо угадывать по конвенции — угадали неверно (или колонка
|
||||
// после переименования Status → RequestStatus ещё не переиндексирована), и
|
||||
// фильтр по статусу молча возвращал 0 заявок. Поэтому статус заявки
|
||||
// фильтруется НЕ в KQL, а в памяти после дочитывания реальных полей через REST
|
||||
// (см. getOwnRequestsByStatus) — Search там ищет только по надёжному
|
||||
// AuthorOWSUSER, статус ни при чём.
|
||||
const MP_STATUS = 'ApprovalStatusOWSCHCS';
|
||||
|
||||
// То же самое для Choice-колонки «Статус» списка ДАННЫХ (внутреннее имя
|
||||
// RequestStatus, см. forms/*/provisioning.json) — по ней отбираются
|
||||
// «Доработка» и фильтр по статусу в «Моих заявках». Кириллица в значении
|
||||
// проблемы не создаёт: KQL — не $filter (там она действительно подводит, см.
|
||||
// indexApprovalsForType), и по MP_STATUS выше точно так же ищутся русские
|
||||
// статусы строк маршрута.
|
||||
// Если отбор по статусу вдруг начнёт стабильно возвращать 0 — проверять надо в
|
||||
// первую очередь ЭТО имя в схеме поиска (см. предупреждение в searchOwnRequests).
|
||||
const MP_TICKET_STATUS = 'RequestStatusOWSCHCS';
|
||||
|
||||
export function currentUserEmail(context: WebPartContext): string {
|
||||
return (context.pageContext.user.email || '').toLowerCase();
|
||||
}
|
||||
@@ -210,7 +208,7 @@ export async function getNavCounts(context: WebPartContext): Promise<INavCounts>
|
||||
|
||||
const [turn, rework] = await Promise.all([
|
||||
buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES),
|
||||
getOwnRequests(context, types, 1, REQUEST_STATUS.returned),
|
||||
getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -593,8 +591,7 @@ async function loadByIds(
|
||||
// Search — сырьё для довеска по свежести (см. getFreshOwnRowsForType).
|
||||
// Фильтруем только по AuthorId: статус в $filter не используем, потому что
|
||||
// кириллица в сравнении там уже подводила (см. indexApprovalsForType) — отбор
|
||||
// по статусу делает вызывающая сторона. В самом Search кириллица проблемой не
|
||||
// является, там статус уходит прямо в KQL (см. MP_TICKET_STATUS).
|
||||
// по статусу делает вызывающая сторона.
|
||||
async function getOwnRowsForType(context: WebPartContext, type: IRequestType): Promise<IMyRequest[]> {
|
||||
try {
|
||||
const sp = spForSite(type.siteUrl as string, context);
|
||||
@@ -636,6 +633,39 @@ async function getFreshOwnRowsForType(context: WebPartContext, type: IRequestTyp
|
||||
);
|
||||
}
|
||||
|
||||
// Заявки автора, изменённые за последние RECENT_MS, — довесок к Search для
|
||||
// getOwnRequestsByStatus. В отличие от getFreshOwnRowsForType (которая ловит
|
||||
// НОВЫЕ заявки по дате СОЗДАНИЯ), здесь важна дата ИЗМЕНЕНИЯ: заявку могли
|
||||
// вернуть на доработку неделю спустя после создания — по Created такую не
|
||||
// найти, а Modified у неё обновится ровно в момент решения согласующего
|
||||
// (см. approverDecision.service.ts/decide). Filter по Modified — обычная дата,
|
||||
// не Choice-колонка, кириллицы в значении нет, поэтому $filter здесь надёжен
|
||||
// (в отличие от статуса, см. getOwnRequestsByStatus).
|
||||
async function getRecentlyChangedOwnRowsForType(context: WebPartContext, type: IRequestType): Promise<IMyRequest[]> {
|
||||
try {
|
||||
const sp = spForSite(type.siteUrl as string, context);
|
||||
const me = await sp.web.currentUser.select('Id')();
|
||||
const cutoffIso = new Date(Date.now() - RECENT_MS).toISOString();
|
||||
|
||||
const rows = (await sp.web
|
||||
.getList(type.listId as string)
|
||||
.items.filter(`AuthorId eq ${me.Id} and Modified ge '${cutoffIso}'`)
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(OWN_REST_TOP)()) as Array<Record<string, unknown>>;
|
||||
|
||||
return rows.map((r) =>
|
||||
toMyRequest(r, r.Author as { EMail?: string; Title?: string } | undefined, type, 'own')
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[Текущие заявки] «${type.name}»: не удалось прочитать недавно изменённые заявки:`,
|
||||
(e as Error).message
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// «Мои заявки» БЕЗ поиска — гибрид для масштаба: Search с серверной пагинацией
|
||||
// (RowLimit/StartRow) находит страницу без сканирования всего архива, REST по
|
||||
// «последним N минутам» добирает то, что Search мог не успеть проиндексировать
|
||||
@@ -644,20 +674,18 @@ async function getFreshOwnRowsForType(context: WebPartContext, type: IRequestTyp
|
||||
export async function getOwnRequests(
|
||||
context: WebPartContext,
|
||||
types: IRequestType[],
|
||||
page: number,
|
||||
status: string = ''
|
||||
page: number
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||
|
||||
const searchPage = await searchOwnRequests(context, types, '', page, status);
|
||||
const searchPage = await searchOwnRequests(context, types, '', page);
|
||||
|
||||
if (page !== 1) return searchPage;
|
||||
|
||||
// Довесок по свежести отбираем по статусу здесь: он приходит REST-ом, где
|
||||
// сравнивать статус в $filter нельзя (кириллица), — зато набор маленький.
|
||||
const freshRows = (await Promise.all(types.map((t) => getFreshOwnRowsForType(context, t))))
|
||||
.reduce((all, r) => all.concat(r), [] as IMyRequest[])
|
||||
.filter((r) => !status || r.status === status);
|
||||
const freshRows = (await Promise.all(types.map((t) => getFreshOwnRowsForType(context, t)))).reduce(
|
||||
(all, r) => all.concat(r),
|
||||
[] as IMyRequest[]
|
||||
);
|
||||
|
||||
const searchKeys = new Set(searchPage.rows.map((r) => `${r.requestTypeId}-${r.id}`));
|
||||
const onlyFresh = freshRows.filter((r) => !searchKeys.has(`${r.requestTypeId}-${r.id}`));
|
||||
@@ -677,8 +705,7 @@ export async function searchOwnRequests(
|
||||
context: WebPartContext,
|
||||
types: IRequestType[],
|
||||
query: string,
|
||||
page: number,
|
||||
status: string = ''
|
||||
page: number
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||
|
||||
@@ -689,9 +716,6 @@ export async function searchOwnRequests(
|
||||
`(${types.map((t) => `Path:"${listAbsUrl(t)}*"`).join(' OR ')})`,
|
||||
`AuthorOWSUSER:"${claimsLogin}"`,
|
||||
];
|
||||
// Отбор по статусу заявки — на СЕРВЕРЕ, вместе с постраничностью: иначе
|
||||
// пришлось бы вычитывать весь архив автора и резать его в памяти.
|
||||
if (status) clauses.push(`${MP_TICKET_STATUS}:"${status}"`);
|
||||
const text = freeTextClause(query);
|
||||
if (text) clauses.push(text);
|
||||
const kql = clauses.join(' AND ');
|
||||
@@ -708,18 +732,6 @@ export async function searchOwnRequests(
|
||||
|
||||
|
||||
const totalRows = results.TotalRows || 0;
|
||||
|
||||
// Отбор по статусу опирается на managed property, имя которой мы выводим по
|
||||
// конвенции, а не читаем из схемы (см. MP_TICKET_STATUS). Если она названа
|
||||
// иначе или не заведена, запрос отработает без ошибки и просто вернёт 0 —
|
||||
// такое молчание слишком дорого отлаживать вслепую, поэтому подсказываем.
|
||||
if (status && totalRows === 0) {
|
||||
console.warn(
|
||||
`[Мои заявки] По статусу «${status}» ничего не найдено. Если так на ЛЮБОМ статусе — ` +
|
||||
`проверьте managed property «${MP_TICKET_STATUS}» в схеме поиска SharePoint.`
|
||||
);
|
||||
}
|
||||
|
||||
const entries = hitsToEntries(results, types);
|
||||
const byKey = entries.length > 0 ? await loadByIds(context, entries, 'own') : new Map<string, IMyRequest>();
|
||||
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
||||
@@ -728,6 +740,51 @@ export async function searchOwnRequests(
|
||||
return { rows, totalRows };
|
||||
}
|
||||
|
||||
// Свои заявки с конкретным статусом ЗАЯВКИ (не строки маршрута) — «Доработка»
|
||||
// и фильтр по статусу на вкладке «Инициированные».
|
||||
//
|
||||
// Статус НЕ уходит в KQL: единственная надёжная managed property здесь —
|
||||
// AuthorOWSUSER (стандартное поле автора), а для произвольной Choice-колонки
|
||||
// имя managed property надо либо знать из схемы поиска, либо угадывать по
|
||||
// конвенции — угадали неверно (или она ещё не переиндексирована после
|
||||
// переименования колонки Status → RequestStatus), из-за чего фильтр по
|
||||
// статусу молча возвращал 0. Поэтому здесь Search ищет ТОЛЬКО по автору
|
||||
// (+ опционально текст) — это находит ВСЕ заявки автора вплоть до
|
||||
// KQL_ID_LIMIT, — а отбор по статусу и постраничность делаются в памяти по
|
||||
// уже дочитанным через REST реальным полям (см. searchOwnAll/loadByIds).
|
||||
// Тот же принцип, что и в filterPendingByQuery: Search сужает набор по
|
||||
// надёжному полю, остальное — на клиенте.
|
||||
export async function getOwnRequestsByStatus(
|
||||
context: WebPartContext,
|
||||
types: IRequestType[],
|
||||
status: string,
|
||||
page: number,
|
||||
query: string = ''
|
||||
): Promise<IOwnRequestsPage> {
|
||||
const all = await searchOwnAll(context, types, query);
|
||||
const searchFiltered = all.filter((r) => r.status === status);
|
||||
|
||||
// Довесок по недавним изменениям — только на первой странице и только без
|
||||
// текстового поиска (см. getOwnRequests: тот же принцип — довесок относится
|
||||
// к «показать всё, свежее по дате», а не к результату полнотекстового поиска).
|
||||
let merged = searchFiltered;
|
||||
if (page === 1 && !query.trim()) {
|
||||
const freshRows = (await Promise.all(types.map((t) => getRecentlyChangedOwnRowsForType(context, t))))
|
||||
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
|
||||
.filter((r) => r.status === status);
|
||||
|
||||
const seen = new Set(searchFiltered.map((r) => `${r.requestTypeId}-${r.id}`));
|
||||
const onlyFresh = freshRows.filter((r) => !seen.has(`${r.requestTypeId}-${r.id}`));
|
||||
merged = onlyFresh.concat(searchFiltered);
|
||||
}
|
||||
|
||||
const sorted = merged.sort((a, b) => b.created.localeCompare(a.created));
|
||||
return {
|
||||
rows: sorted.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||
totalRows: sorted.length,
|
||||
};
|
||||
}
|
||||
|
||||
// «На согласовании»: список заявок уже отобран по строкам согласования
|
||||
// (getPendingApprovalIndex — там же определяется, чья сейчас очередь,
|
||||
// обычным REST). Здесь среди НИХ фильтруем по тексту — только через Search
|
||||
@@ -791,7 +848,7 @@ export interface ISearchPage {
|
||||
|
||||
// Как searchOwnRequests, но без $skip/$top — сразу весь найденный срез (до
|
||||
// KQL_ID_LIMIT). Только Search, без REST-подстраховки на свежесть.
|
||||
async function searchOwnAll(
|
||||
export async function searchOwnAll(
|
||||
context: WebPartContext,
|
||||
types: IRequestType[],
|
||||
query: string
|
||||
|
||||
Reference in New Issue
Block a user