новая статусная модель

This commit is contained in:
gorbenko.pavel
2026-08-05 12:28:58 +05:00
parent 5bc80df11c
commit 7fa0a800d8
8 changed files with 515 additions and 216 deletions
@@ -1,6 +1,13 @@
import * as React from 'react';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import { getLinkedTypes, getOwnRequests, getPendingApproval } from './myRequests.service';
import {
filterPendingByQuery,
getLinkedTypes,
getOwnRequests,
getPendingApproval,
getPendingApprovalIndex,
hydratePendingAuthors,
} from './myRequests.service';
import { IMyRequest } from './models/IMyRequest';
import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
import { onRequestsChanged } from '../../shared/requestEvents';
@@ -166,15 +173,24 @@ const Table: React.FC<{
</div>
);
// Раздел с таблицей заявок: поиск по названию + постраничный вывод.
// Раздел с таблицей заявок: поиск по всем полям + вывод постранично.
//
// 'approvals' грузится целиком (алгоритм «чья сейчас очередь» требует видеть
// весь маршрут заявки), поиск и пагинация — на клиенте.
// 'own' идёт постранично через обычный REST (см. myRequests.service), поиск —
// серверным OData-фильтром по Title.
// 'own' — Search сразу с пагинацией (RowLimit/StartRow): автор = текущий
// пользователь + текст запроса по всем полям.
//
// 'approvals' без поиска — обычный REST, отфильтрованный по ApproverId(+Status),
// страница нарезается на СЕРВЕРЕ (getPendingApproval сам берёт нужный срез
// индекса и дочитывает автора только для него).
// 'approvals' с поиском — область расширяется до «где пользователь вообще есть
// в маршруте» (getPendingApprovalIndex, ПОЛНЫЙ список без автора — постранично
// его тут не взять, т.к. фильтрация текстом идёт уже после), затем текстовый
// фильтр через Search (filterPendingByQuery), и только после фильтрации —
// нарезка на страницы и дочитывание автора (hydratePendingAuthors) — тоже
// только для видимой страницы, а не для всего отфильтрованного набора.
//
// Оба варианта перечитываются по событию rp-requests-changed
// (shared/requestEvents) — после создания заявки и после решения согласующего.
// Поиск идёт по индексу, поэтому свежая правка может проявиться не мгновенно.
export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({ context, kind, onOpen }) => {
const [types, setTypes] = React.useState<IRequestType[] | undefined>(undefined);
const [rows, setRows] = React.useState<IMyRequest[] | undefined>(undefined);
@@ -203,44 +219,63 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({ context,
if (types === undefined) return;
let cancelled = false;
const load = (): void => {
const request =
kind === 'own'
? getOwnRequests(context, types, query, page).then((r) => ({ rows: r.rows, total: r.totalRows }))
: getPendingApproval(context).then((r) => {
console.table(r.debug);
return { rows: r.rows, total: r.rows.length };
});
request
.then((result) => {
const load = async (): Promise<void> => {
try {
if (kind === 'own') {
const r = await getOwnRequests(context, types, query, page);
if (cancelled) return;
setRows(result.rows);
setTotalRows(result.total);
setRows(r.rows);
setTotalRows(r.totalRows);
setError('');
})
.catch((e) => {
if (!cancelled) setError((e as Error).message);
});
return;
}
const hasQuery = query.trim().length > 0;
if (!hasQuery) {
// Без поиска — страница уже нарезана и дочитана сервером.
const r = await getPendingApproval(context, 'myTurn', page);
if (cancelled) return;
console.table(r.debug);
setRows(r.rows);
setTotalRows(r.totalRows);
setError('');
return;
}
// С поиском — сначала полный список «где я есть в маршруте» (без
// автора), фильтруем текстом, и только потом режем на страницы и
// дочитываем автора — только для видимой страницы.
const idx = await getPendingApprovalIndex(context, 'involved');
if (cancelled) return;
console.table(idx.debug);
const filtered = await filterPendingByQuery(context, idx.types, idx.rows, query);
if (cancelled) return;
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
await hydratePendingAuthors(context, idx.types, pageRows);
if (cancelled) return;
setRows(pageRows);
setTotalRows(filtered.length);
setError('');
} catch (e) {
if (!cancelled) setError((e as Error).message);
}
};
load();
const unsubscribe = onRequestsChanged(load);
load().catch(() => undefined); // ошибки уже обработаны внутри load через setError
const unsubscribe = onRequestsChanged(() => {
load().catch(() => undefined);
});
return () => {
cancelled = true;
unsubscribe();
};
}, [context, types, kind, query, page]);
// 'approvals' приходит целиком — фильтруем и нарезаем страницы на клиенте.
const q = query.trim().toLowerCase();
let visible = rows || [];
let total = totalRows;
if (kind === 'approvals' && rows) {
const filtered = q ? rows.filter((r) => r.title.toLowerCase().indexOf(q) !== -1) : rows;
total = filtered.length;
visible = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
}
// Обе ветки теперь отдают уже нарезанную страницу — просто показываем как есть.
const q = query.trim();
const total = totalRows;
const visible = rows || [];
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const heading = kind === 'approvals' ? 'Ждут вашего решения' : 'Мои заявки';
@@ -255,7 +290,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({ context,
<input
type="search"
className={styles.search}
placeholder="Поиск по названию…"
placeholder="Поиск по заявкам…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>