переработка выбора
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
ManagedRequestMatrix,
|
||||
ManagedRequestPage,
|
||||
ManagedRequestSummary,
|
||||
RequestType
|
||||
RequestListPartition
|
||||
} from '../models';
|
||||
|
||||
// Список данных «<Форма> — данные» → список согласования «<Форма> — согласование»
|
||||
@@ -86,15 +86,19 @@ interface IRawDataRow {
|
||||
RequestStatus?: string;
|
||||
Created?: string;
|
||||
Author?: { Title?: string };
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- зеркалит SharePoint: колонки может не быть в старых списках/строках
|
||||
RequestUniqueCode?: string | null;
|
||||
}
|
||||
|
||||
function toSummary(r: IRawDataRow): ManagedRequestSummary {
|
||||
function toSummary(r: IRawDataRow, listId: string): ManagedRequestSummary {
|
||||
return {
|
||||
id: r.Id,
|
||||
title: r.Title || `#${r.Id}`,
|
||||
status: r.RequestStatus || '',
|
||||
created: r.Created || '',
|
||||
authorName: r.Author?.Title || ''
|
||||
authorName: r.Author?.Title || '',
|
||||
uniqueCode: r.RequestUniqueCode || null,
|
||||
listId
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,24 +128,41 @@ export class RequestManageService {
|
||||
return { dataListTitle: info.Title, approvalListTitle };
|
||||
}
|
||||
|
||||
// К какой партиции относится хит Search — по префиксу его Path. Партиции
|
||||
// определяются вызывающей стороной (ListPartitionService.ListAsync), их
|
||||
// список короткий (единицы), линейный поиск тут не проблема.
|
||||
private partitionForPath(
|
||||
siteUrl: string,
|
||||
partitions: RequestListPartition[],
|
||||
path: string
|
||||
): RequestListPartition | undefined {
|
||||
return partitions.filter(p => path.indexOf(listAbsUrl(siteUrl, p.listId)) === 0)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Заявки типа requestType постранично, для комбобокса выбора заявки. Сначала
|
||||
* Search по названию (свободный терм, пустой query — просто последние без
|
||||
* фильтра), затем дочитывание полей REST'ом по найденным Id — Search не
|
||||
* отдаёт произвольные поля списка надёжно (та же двухшаговая схема, что в
|
||||
* request-portal).
|
||||
* Заявки по ВСЕМ партициям списка данных этого типа (архив + текущая)
|
||||
* постранично, для комбобокса выбора заявки. Сначала Search по названию
|
||||
* (свободный терм, пустой query — просто последние без фильтра), затем
|
||||
* дочитывание полей REST'ом по найденным Id — Search не отдаёт произвольные
|
||||
* поля списка надёжно (та же двухшаговая схема, что в request-portal).
|
||||
*
|
||||
* Заявки, набранные ростом одного типа, физически «переезжают» на новый
|
||||
* список-партицию (см. ТЗ по архивации) — искать нужно сразу по всем, иначе
|
||||
* заявки из архивных партиций в комбобоксе просто не найти.
|
||||
*/
|
||||
public async searchRequests(
|
||||
requestType: RequestType,
|
||||
partitions: RequestListPartition[],
|
||||
siteUrl: string,
|
||||
query: string,
|
||||
page: number,
|
||||
pageSize = 20
|
||||
): Promise<ManagedRequestPage> {
|
||||
if (!requestType.listId || !requestType.siteUrl) return { items: [], hasMore: false };
|
||||
if (partitions.length === 0) return { items: [], hasMore: false };
|
||||
|
||||
const sp = this.clientFor(requestType.siteUrl);
|
||||
const absUrl = listAbsUrl(requestType.siteUrl, requestType.listId);
|
||||
const clauses = [`Path:"${absUrl}*"`];
|
||||
const sp = this.clientFor(siteUrl);
|
||||
const pathClauses = partitions.map(p => `Path:"${listAbsUrl(siteUrl, p.listId)}*"`);
|
||||
const pathClause = pathClauses.length > 1 ? `(${pathClauses.join(' OR ')})` : pathClauses[0];
|
||||
const clauses = [pathClause];
|
||||
const text = freeTextClause(query);
|
||||
if (text) clauses.push(text);
|
||||
|
||||
@@ -154,65 +175,83 @@ export class RequestManageService {
|
||||
SortList: [{ Property: 'Created', Direction: 1 }]
|
||||
});
|
||||
|
||||
const rawRows = (result.PrimarySearchResults ?? []) as Array<{ ListItemID?: string }>;
|
||||
// Дедуп по Id обязателен: TrimDuplicates: false выше выключает штатную
|
||||
// дедупликацию Search (она иногда схлопывает нужные нам заявки), и тогда
|
||||
// индекс может отдать один и тот же элемент несколькими хитами
|
||||
// (переиндексация, несколько «версий» в crawl). Без этого фильтра хиты
|
||||
// превращались в пункты комбобокса один-в-один, и заявка показывалась в
|
||||
// списке дважды — с одинаковым React-ключом. Та же причина и то же лечение,
|
||||
// что в request-portal (features/myRequests/myRequests.service.ts/hitsToEntries).
|
||||
const seen = new Set<number>();
|
||||
const ids = rawRows
|
||||
.map(r => Number(r.ListItemID))
|
||||
.filter(id => !isNaN(id) && id > 0)
|
||||
.filter(id => {
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
if (ids.length === 0) return { items: [], hasMore: false };
|
||||
const rawRows = (result.PrimarySearchResults ?? []) as Array<{ ListItemID?: string; Path?: string }>;
|
||||
// Дедуп обязателен по паре (listId, Id), а НЕ по одному Id: у разных
|
||||
// партиций независимая нумерация элементов (см. ТЗ по архивации), поэтому
|
||||
// одинаковый номер в разных партициях — это две РАЗНЫЕ заявки. TrimDuplicates:
|
||||
// false выше выключает штатную дедупликацию Search (она иногда схлопывает
|
||||
// нужные нам заявки), и тогда индекс может отдать один и тот же элемент
|
||||
// несколькими хитами (переиндексация, несколько «версий» в crawl) — та же
|
||||
// причина и то же лечение, что в request-portal
|
||||
// (features/myRequests/myRequests.service.ts/hitsToEntries).
|
||||
const seen = new Set<string>();
|
||||
const hits: Array<{ id: number; partition: RequestListPartition }> = [];
|
||||
rawRows.forEach(r => {
|
||||
const id = Number(r.ListItemID);
|
||||
if (isNaN(id) || id <= 0 || !r.Path) return;
|
||||
const partition = this.partitionForPath(siteUrl, partitions, r.Path);
|
||||
if (!partition) return;
|
||||
const key = `${partition.listId}|${id}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
hits.push({ id, partition });
|
||||
});
|
||||
if (hits.length === 0) return { items: [], hasMore: false };
|
||||
|
||||
const list = sp.web.getList(requestType.listId);
|
||||
const dataRows = (await list.items
|
||||
.filter(ids.map(id => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(ids.length)()) as IRawDataRow[];
|
||||
const idsByListId = new Map<string, number[]>();
|
||||
hits.forEach(h => idsByListId.set(h.partition.listId, [...(idsByListId.get(h.partition.listId) || []), h.id]));
|
||||
|
||||
const rowByKey = new Map<string, IRawDataRow>();
|
||||
await Promise.all(
|
||||
Array.from(idsByListId.entries()).map(async ([listId, ids]) => {
|
||||
const list = sp.web.getList(listId);
|
||||
const dataRows = (await list.items
|
||||
.filter(ids.map(id => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title', 'RequestUniqueCode')
|
||||
.expand('Author')
|
||||
.top(ids.length)()) as IRawDataRow[];
|
||||
dataRows.forEach(r => rowByKey.set(`${listId}|${r.Id}`, r));
|
||||
})
|
||||
);
|
||||
|
||||
const byId = new Map(dataRows.map(r => [r.Id, r]));
|
||||
// Порядок задаёт Search (SortList по дате), не порядок ответа REST.
|
||||
const items = ids
|
||||
.map(id => byId.get(id))
|
||||
.filter((r): r is IRawDataRow => !!r)
|
||||
.map(toSummary);
|
||||
const items = hits
|
||||
.map(h => {
|
||||
const row = rowByKey.get(`${h.partition.listId}|${h.id}`);
|
||||
return row ? toSummary(row, h.partition.listId) : undefined;
|
||||
})
|
||||
.filter((r): r is ManagedRequestSummary => !!r);
|
||||
|
||||
const totalRows = (result as unknown as { TotalRows?: number }).TotalRows ?? items.length;
|
||||
return { items, hasMore: page * pageSize < totalRows };
|
||||
}
|
||||
|
||||
/** Заявка + полный маршрут (обе фазы, по StepOrder) — для экрана редактирования. */
|
||||
public async getMatrix(requestType: RequestType, requestId: number): Promise<ManagedRequestMatrix> {
|
||||
if (!requestType.listId || !requestType.siteUrl) {
|
||||
throw new Error('У типа заявки не указан список SharePoint.');
|
||||
}
|
||||
const sp = this.clientFor(requestType.siteUrl);
|
||||
const dataList = sp.web.getList(requestType.listId);
|
||||
/**
|
||||
* Заявка + полный маршрут (обе фазы, по StepOrder) — для экрана редактирования.
|
||||
*
|
||||
* listId — КОНКРЕТНАЯ партиция, где физически лежит заявка (из
|
||||
* ManagedRequestSummary.listId, отданного searchRequests/предыдущей
|
||||
* getMatrix) — не requestType.listId: та партиция может быть уже не той, где
|
||||
* лежит эта заявка (см. ТЗ по архивации).
|
||||
*/
|
||||
public async getMatrix(siteUrl: string, listId: string, requestId: number): Promise<ManagedRequestMatrix> {
|
||||
const sp = this.clientFor(siteUrl);
|
||||
const dataList = sp.web.getList(listId);
|
||||
|
||||
const [requestRow, { dataListTitle, approvalListTitle }] = await Promise.all([
|
||||
dataList.items
|
||||
.getById(requestId)
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title')
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/Title', 'RequestUniqueCode')
|
||||
.expand('Author')() as Promise<IRawDataRow>,
|
||||
this.listTitles(sp, requestType.listId)
|
||||
this.listTitles(sp, listId)
|
||||
]);
|
||||
|
||||
const dataListUrl = listAbsUrl(requestType.siteUrl, requestType.listId);
|
||||
const dataListUrl = listAbsUrl(siteUrl, listId);
|
||||
|
||||
if (!approvalListTitle) {
|
||||
return {
|
||||
request: toSummary(requestRow),
|
||||
siteUrl: requestType.siteUrl,
|
||||
request: toSummary(requestRow, listId),
|
||||
siteUrl,
|
||||
dataListTitle,
|
||||
dataListUrl,
|
||||
approvalListTitle: '',
|
||||
@@ -264,7 +303,7 @@ export class RequestManageService {
|
||||
};
|
||||
});
|
||||
|
||||
return { request: toSummary(requestRow), siteUrl: requestType.siteUrl, dataListTitle, dataListUrl, approvalListTitle, rows };
|
||||
return { request: toSummary(requestRow, listId), siteUrl, dataListTitle, dataListUrl, approvalListTitle, rows };
|
||||
}
|
||||
|
||||
/** email → числовой Id пользователя сайта (Person-поле требует Id, не email). */
|
||||
|
||||
Reference in New Issue
Block a user