feat: changed lists columns
This commit is contained in:
@@ -12,8 +12,12 @@ import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||
const FIELDS = {
|
||||
approver: 'Approver', // «Согласующий» (User) → ApproverId
|
||||
status: 'Status', // «Статус» (Choice)
|
||||
// Внутренние имена с префиксом группы колонок сайта (TansuSite) и «Request» —
|
||||
// так они заведены в SharePoint. Из них SharePoint выводит имена managed
|
||||
// properties для поиска: TansuSiteApproverOWSUSER и RequestStatusOWSCHCS
|
||||
// (см. MP_APPROVER/MP_STATUS в features/myRequests/myRequests.service.ts).
|
||||
approver: 'TansuSiteApprover', // «Согласующий» (User) → TansuSiteApproverId
|
||||
status: 'RequestStatus', // «Статус» (Choice)
|
||||
ticket: 'Ticket', // «Заявка» (Lookup) → TicketId
|
||||
date: 'Date', // «Дата» (DateTime)
|
||||
comment: 'Comment', // «Комментарий» (Text)
|
||||
@@ -28,6 +32,12 @@ export interface IApproverToWrite {
|
||||
email: string;
|
||||
position: string;
|
||||
allowReassign: boolean;
|
||||
/** Порядок (StepOrder). Не задан — берётся позиция в массиве (orderStart + индекс).
|
||||
* Несколько согласующих с ОДИНАКОВЫМ order — параллельный шаг: все становятся
|
||||
* «На согласовании» одновременно (см. createSteps). Сейчас редактор матрицы
|
||||
* (ApprovalMatrix/useMatrixEditor) всегда даёт уникальный order по позиции —
|
||||
* это поле для параллельных шагов, приходящих из внешнего маршрута напрямую. */
|
||||
order?: number;
|
||||
}
|
||||
|
||||
// Все запросы к списку «<форма> — согласование». listTitle задаётся формой.
|
||||
@@ -38,9 +48,14 @@ export class ApprovalListService {
|
||||
return getSP().web.lists.getByTitle(this.listTitle);
|
||||
}
|
||||
|
||||
// Пишет по строке на каждого участника фазы с указанным статусом.
|
||||
// Порядок (Order) = orderStart + позиция в массиве (фазы идут подряд в одном списке:
|
||||
// сначала согласующие 1..N, затем исполнители N+1..N+M).
|
||||
// Пишет по строке на каждого участника фазы. Порядок (Order) = a.order, если
|
||||
// задан, иначе orderStart + позиция в массиве (фазы идут подряд в одном
|
||||
// списке: сначала согласующие 1..N, затем исполнители N+1..N+M).
|
||||
//
|
||||
// Статус пишется переданным значением ОДИНАКОВО для всех строк партии — кто
|
||||
// сейчас «на очереди», а кто «ждёт», решает отдельно activateNextQueued
|
||||
// (вызывается сразу после createSteps в approvalSubmission.service.ts/writeRoute
|
||||
// и в approverDecision.service.ts/decide).
|
||||
public async createSteps(
|
||||
requestId: number,
|
||||
approvers: IApproverToWrite[],
|
||||
@@ -48,22 +63,65 @@ export class ApprovalListService {
|
||||
phase: string = APPROVAL_PHASE.approval,
|
||||
orderStart: number = 1
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < approvers.length; i++) {
|
||||
const a = approvers[i];
|
||||
if (!a.email) continue;
|
||||
const withOrder = approvers
|
||||
.map((a, i) => ({ a, order: a.order ?? orderStart + i }))
|
||||
.filter((x) => !!x.a.email);
|
||||
|
||||
for (const { a, order } of withOrder) {
|
||||
const approverId = await this.resolveUserId(a.email);
|
||||
await this.list().items.add({
|
||||
Title: a.position || a.email,
|
||||
[`${FIELDS.approver}Id`]: approverId,
|
||||
[FIELDS.status]: status,
|
||||
[`${FIELDS.ticket}Id`]: requestId,
|
||||
[FIELDS.order]: orderStart + i,
|
||||
[FIELDS.order]: order,
|
||||
[FIELDS.allowReassign]: a.allowReassign,
|
||||
[FIELDS.phase]: phase,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Продвигает очередь: среди ещё не активных строк со статусом «Ждёт очереди»
|
||||
// в текущей фазе активирует те, что делят наименьший StepOrder (обычно одна
|
||||
// строка; несколько — параллельный шаг). Фаза исполнения активируется, только
|
||||
// когда фаза согласования пройдена целиком (без Отклонено/Возвращена) — иначе
|
||||
// строки исполнения остаются «Ждёт очереди». Если фаза согласования уже
|
||||
// остановлена решением, ничего не активирует.
|
||||
public async activateNextQueued(requestId: number): Promise<void> {
|
||||
const rows = await this.getForRequest(requestId);
|
||||
const approvalRows = rows.filter((r) => r.phase !== APPROVAL_PHASE.execution);
|
||||
const execRows = rows.filter((r) => r.phase === APPROVAL_PHASE.execution);
|
||||
|
||||
const approvalStopped = approvalRows.some(
|
||||
(r) => r.status === REQUEST_STATUS.rejected || r.status === REQUEST_STATUS.returned
|
||||
);
|
||||
if (approvalStopped) return;
|
||||
|
||||
const approvalDone =
|
||||
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
|
||||
|
||||
const activate = async (phaseRows: IApprovalRow[], activeStatus: string): Promise<void> => {
|
||||
if (phaseRows.some((r) => r.status === activeStatus)) return;
|
||||
const queuedRows = phaseRows.filter((r) => r.status === REQUEST_STATUS.queued);
|
||||
if (queuedRows.length === 0) return;
|
||||
const minOrder = Math.min(...queuedRows.map((r) => r.order));
|
||||
await Promise.all(
|
||||
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setStatus(r.id, activeStatus))
|
||||
);
|
||||
};
|
||||
|
||||
if (!approvalDone) {
|
||||
await activate(approvalRows, REQUEST_STATUS.onApproval);
|
||||
} else {
|
||||
await activate(execRows, REQUEST_STATUS.onExecution);
|
||||
}
|
||||
}
|
||||
|
||||
// Точечное обновление статуса одной строки маршрута.
|
||||
public async setStatus(rowId: number, status: string): Promise<void> {
|
||||
await this.list().items.getById(rowId).update({ [FIELDS.status]: status });
|
||||
}
|
||||
|
||||
// Добавляет одну строку согласования с заданным Order (для делегата).
|
||||
public async addApproverRow(
|
||||
requestId: number,
|
||||
@@ -187,19 +245,6 @@ export class ApprovalListService {
|
||||
});
|
||||
}
|
||||
|
||||
// Активирует следующий шаг маршрута: первую по очереди строку со статусом
|
||||
// «Ждёт очереди» переводит в активный статус её фазы (Согласование → «На
|
||||
// согласовании», Исполнение → «На исполнении»). Если ждущих нет — ничего.
|
||||
// Вызывается при отправке (активировать первый шаг) и после каждого решения.
|
||||
public async activateNextQueued(requestId: number): Promise<void> {
|
||||
const rows = await this.getForRequest(requestId); // отсортированы по StepOrder
|
||||
const next = rows.filter((r) => r.status === REQUEST_STATUS.queued)[0];
|
||||
if (!next) return;
|
||||
const active =
|
||||
next.phase === APPROVAL_PHASE.execution ? REQUEST_STATUS.onExecution : REQUEST_STATUS.onApproval;
|
||||
await this.list().items.getById(next.id).update({ [FIELDS.status]: active });
|
||||
}
|
||||
|
||||
// Удаляет все строки согласования по заявке (при возврате/повторной отправке).
|
||||
public async deleteForRequest(requestId: number): Promise<void> {
|
||||
const rows = await this.getForRequest(requestId);
|
||||
|
||||
@@ -43,13 +43,24 @@ export async function writeApprovers(
|
||||
status: string
|
||||
): Promise<void> {
|
||||
await approval.deleteForRequest(requestId);
|
||||
await approval.createSteps(requestId, approvers, status, APPROVAL_PHASE.approval, 1);
|
||||
const submitted = status === REQUEST_STATUS.onApproval;
|
||||
const rowStatus = submitted ? REQUEST_STATUS.queued : status;
|
||||
await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1);
|
||||
if (submitted) {
|
||||
await approval.activateNextQueued(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
// Записать ОБЕ фазы маршрута заявки: согласующих (Order 1..N) и исполнителей
|
||||
// (Order N+1..N+M) в один список. Исполнители пишутся сразу при отправке —
|
||||
// стороне согласующего внешний API уже не нужен, переход в фазу исполнения
|
||||
// делает computeRequestStatus, когда все согласующие одобрили.
|
||||
//
|
||||
// Исполнители стартуют ВСЕ как «Ждёт очереди» (rowStatus, та же логика, что и
|
||||
// у согласующих) — до прохождения фазы согласования у исполнителей нет текущей
|
||||
// очереди. Первого исполнителя переключит на «На исполнении»
|
||||
// approval.activateNextQueued, когда фаза согласования будет пройдена целиком
|
||||
// (см. approverDecision.service.ts/decide).
|
||||
export async function writeRoute(
|
||||
approval: ApprovalListService,
|
||||
requestId: number,
|
||||
|
||||
@@ -32,17 +32,20 @@ export class ApproverService {
|
||||
) {}
|
||||
|
||||
// Ожидающая строка текущего пользователя, если сейчас его очередь; иначе undefined.
|
||||
// Последовательное согласование: активна первая строка со статусом «На согласовании».
|
||||
// Очередь уже проставлена на запись (см. createSteps/advanceQueue) — активна
|
||||
// ровно одна строка со статусом «На согласовании» на фазу, искать «первую по
|
||||
// порядку» не нужно.
|
||||
public async getPendingRowForCurrentUser(requestId: number): Promise<IApprovalRow | undefined> {
|
||||
const rows = await this.approval.getForRequest(requestId);
|
||||
const userId = await this.approval.getCurrentUserId();
|
||||
// Активна первая незакрытая строка по очереди: согласующий («На согласовании»)
|
||||
// или, когда согласование пройдено, исполнитель («На исполнении»).
|
||||
const firstPending = rows.filter(
|
||||
// Активные строки текущего шага: согласующий («На согласовании») или, когда
|
||||
// согласование пройдено, исполнитель («На исполнении»). При параллельном шаге
|
||||
// (одинаковый StepOrder) активных строк может быть несколько сразу — ищем
|
||||
// среди ВСЕХ активных, а не только среди первой по порядку.
|
||||
const active = rows.filter(
|
||||
(r) => r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution
|
||||
)[0];
|
||||
if (firstPending && firstPending.approverId === userId) return firstPending;
|
||||
return undefined;
|
||||
);
|
||||
return active.filter((r) => r.approverId === userId)[0];
|
||||
}
|
||||
|
||||
// Делегирование: согласующий добавляет делеганта ПЕРЕД собой (Order — между
|
||||
@@ -59,22 +62,24 @@ export class ApproverService {
|
||||
const prevOrder = before.length > 0 ? before[before.length - 1].order : current.order - 1;
|
||||
const newOrder = (prevOrder + current.order) / 2;
|
||||
const position = await this.approval.getJobTitle(user.loginName);
|
||||
// Делегат наследует фазу той строки, вместо которой действует, и её статус
|
||||
// ожидания (согласование → «На согласовании», исполнение → «На исполнении»).
|
||||
const pendingStatus =
|
||||
current.phase === APPROVAL_PHASE.execution
|
||||
? REQUEST_STATUS.onExecution
|
||||
: REQUEST_STATUS.onApproval;
|
||||
// Делегат наследует фазу той строки, вместо которой согласует (согласование/исполнение).
|
||||
// Обе строки — делегат и прежний согласующий — уходят в «Ждёт очереди»,
|
||||
// activateNextQueued затем активирует делегата (у него меньший order).
|
||||
await this.approval.addApproverRow(
|
||||
requestId,
|
||||
{ email: user.email, position, allowReassign: false },
|
||||
newOrder,
|
||||
pendingStatus,
|
||||
REQUEST_STATUS.queued,
|
||||
current.phase
|
||||
);
|
||||
await this.approval.setStatus(current.id, REQUEST_STATUS.queued);
|
||||
await this.approval.activateNextQueued(requestId);
|
||||
emitRequestsChanged();
|
||||
}
|
||||
|
||||
// Применяет решение: обновляет строку и пересчитывает статус заявки.
|
||||
// Применяет решение: обновляет строку, продвигает очередь (на следующего
|
||||
// согласующего внутри фазы либо на первого исполнителя, если фаза согласования
|
||||
// только что завершилась целиком) и пересчитывает статус заявки.
|
||||
// Возвращает новый статус заявки.
|
||||
public async decide(
|
||||
requestId: number,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export {
|
||||
REQUEST_STATUS,
|
||||
REQUEST_STATUS_CHOICES,
|
||||
APPROVAL_ROW_STATUS_CHOICES,
|
||||
APPROVAL_PHASE,
|
||||
APPROVAL_PHASE_CHOICES,
|
||||
EDITABLE_STATUSES,
|
||||
|
||||
@@ -3,18 +3,28 @@
|
||||
// В слое формы кодом ставятся Draft (черновик) и OnApproval (отправка на согласование);
|
||||
// Approved/Rejected/Returned выставляются при решениях согласующих; Execution/Closed —
|
||||
// зарезервированы под будущие этапы.
|
||||
// Статусы СТРОК списка «— согласование» (не заявки в контентном списке):
|
||||
// draft/rejected/returned — общие с заявкой; остальные — только для строк
|
||||
// маршрута, по фазам:
|
||||
// Согласование: queued («Ждёт очереди») → onApproval («На согласовании») → approved/rejected/returned.
|
||||
// Исполнение: queued → onExecution («На исполнении») → executed («Выполнено»).
|
||||
// Активация — только через ApprovalListService.activateNextQueued (вызывается
|
||||
// при отправке заявки и после каждого положительного решения, см.
|
||||
// approvalSubmission.service.ts/writeRoute, approverDecision.service.ts/decide):
|
||||
// среди строк со статусом queued в текущей фазе находит наименьший StepOrder
|
||||
// и переводит её (или несколько, если делят один order — параллельный шаг) в
|
||||
// onApproval/onExecution. Так «чья сейчас очередь» — просто значение колонки
|
||||
// Status, без пересчёта порядка на клиенте.
|
||||
export const REQUEST_STATUS = {
|
||||
draft: 'Черновик',
|
||||
onApproval: 'На согласовании',
|
||||
queued: 'Ждёт очереди',
|
||||
onExecution: 'На исполнении',
|
||||
executed: 'Выполнено',
|
||||
approved: 'Согласовано',
|
||||
rejected: 'Отклонено',
|
||||
returned: 'Возвращена на доработку',
|
||||
// Статус СТРОКИ маршрута, до которой ещё не дошла очередь (активен только текущий шаг).
|
||||
queued: 'Ждёт очереди',
|
||||
// Статусы СТРОК фазы исполнения: активная очередь исполнителя и выполнено.
|
||||
onExecution: 'На исполнении',
|
||||
executed: 'Выполнено',
|
||||
// Статусы ЗАЯВКИ на этапе исполнения.
|
||||
// Статус ЗАЯВКИ (не строки) на этапе исполнения.
|
||||
execution: 'Выполнение',
|
||||
closed: 'Закрыта',
|
||||
} as const;
|
||||
@@ -33,20 +43,40 @@ export function isEditableStatus(status: string | undefined): boolean {
|
||||
return !status || EDITABLE_STATUSES.indexOf(status) !== -1;
|
||||
}
|
||||
|
||||
// Полный набор значений для Choice-колонки Status при провижининге.
|
||||
// Наборов значений ДВА, и они не совпадают — это разные сущности:
|
||||
//
|
||||
// 1) статус ЗАЯВКИ — колонка Status в списке «… — данные». Пишется формой
|
||||
// (Черновик/На согласовании) и computeRequestStatus после каждого решения.
|
||||
// «Ждёт очереди»/«На исполнении»/«Выполнено» здесь не бывают: это состояния
|
||||
// отдельного шага маршрута, а не заявки целиком.
|
||||
//
|
||||
// 2) статус СТРОКИ МАРШРУТА — колонка RequestStatus в списке «… — согласование».
|
||||
// Пишется createSteps/activateNextQueued/setDecision. «Выполнение»/«Закрыта»
|
||||
// здесь не бывают: это агрегатные состояния заявки, у отдельного шага
|
||||
// смысла не имеют.
|
||||
//
|
||||
// Держать их в синхроне с forms/*/provisioning.json и forms/*/approval/provisioning.json.
|
||||
export const REQUEST_STATUS_CHOICES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
REQUEST_STATUS.onApproval,
|
||||
REQUEST_STATUS.approved,
|
||||
REQUEST_STATUS.rejected,
|
||||
REQUEST_STATUS.returned,
|
||||
REQUEST_STATUS.queued,
|
||||
REQUEST_STATUS.onExecution,
|
||||
REQUEST_STATUS.executed,
|
||||
REQUEST_STATUS.execution,
|
||||
REQUEST_STATUS.closed,
|
||||
];
|
||||
|
||||
export const APPROVAL_ROW_STATUS_CHOICES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
REQUEST_STATUS.queued,
|
||||
REQUEST_STATUS.onApproval,
|
||||
REQUEST_STATUS.onExecution,
|
||||
REQUEST_STATUS.approved,
|
||||
REQUEST_STATUS.executed,
|
||||
REQUEST_STATUS.rejected,
|
||||
REQUEST_STATUS.returned,
|
||||
];
|
||||
|
||||
// Фаза строки в списке маршрута: согласование (кто одобряет) и исполнение
|
||||
// (кто выполняет после согласования). Обе фазы живут в одном списке «— согласование»,
|
||||
// строки различаются колонкой Phase. Значения приходят из внешнего API как
|
||||
|
||||
@@ -4,9 +4,18 @@ import '@pnp/sp/lists';
|
||||
import '@pnp/sp/items';
|
||||
import '@pnp/sp/attachments';
|
||||
import '@pnp/sp/site-users/web';
|
||||
import { BrowserFetchWithRetry } from '@pnp/queryable';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { DATA_SITE_URL } from '../config';
|
||||
|
||||
// Мягкая ретрай-политика вместо стандартной PnPjs (retries: 3, interval: 200).
|
||||
// Дефолт слишком напорист: при отказе (429 «слишком много запросов», 503) он
|
||||
// начинает повторять почти сразу, что при массовой загрузке только усугубляет
|
||||
// throttling со стороны SharePoint. Здесь меньше попыток и заметно длиннее
|
||||
// первая пауза; интервал в PnPjs удваивается с каждой попыткой, так что выходит
|
||||
// примерно 1.5с → 3с.
|
||||
const RETRY = { retries: 2, interval: 1500 };
|
||||
|
||||
// Единый инициализированный клиент PnPjs на весь web part.
|
||||
let _sp: SPFI | undefined;
|
||||
|
||||
@@ -16,7 +25,18 @@ let _sp: SPFI | undefined;
|
||||
* списки живут там, где бы ни стоял сам web part. Авторизация берётся из context.
|
||||
*/
|
||||
export function initSP(context: WebPartContext): void {
|
||||
_sp = spfi(DATA_SITE_URL).using(SPFx(context));
|
||||
_sp = spfi(DATA_SITE_URL).using(SPFx(context), BrowserFetchWithRetry(RETRY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Клиент для ПРОИЗВОЛЬНОГО сайта (списки типов заявок живут на разных сайтах,
|
||||
* см. approval-route API). В отличие от getSP() не кэшируется — адрес сайта
|
||||
* приходит из данных. Ретрай-политика та же, что и у основного клиента:
|
||||
* BrowserFetchWithRetry идёт ПОСЛЕ SPFx, поэтому заменяет его обработчик
|
||||
* отправки (у behavior replace: true по умолчанию).
|
||||
*/
|
||||
export function spForSite(siteUrl: string, context: WebPartContext): SPFI {
|
||||
return spfi(siteUrl).using(SPFx(context), BrowserFetchWithRetry(RETRY));
|
||||
}
|
||||
|
||||
/** Получить инициализированный клиент. Бросает, если initSP не был вызван. */
|
||||
|
||||
@@ -3,7 +3,8 @@ import * as React from 'react';
|
||||
// Маршрутизация портала по параметрам URL.
|
||||
//
|
||||
// Раздел бокового меню — rpnav: 'new' (подать заявку, по умолчанию),
|
||||
// 'approvals' (ждут моего решения), 'own' (мои заявки).
|
||||
// 'approvals' (ждут моего решения), 'own' (мои заявки), 'search' (поиск
|
||||
// сразу по обеим категориям — и по своим заявкам, и по тем, где участвовал).
|
||||
// Открытая заявка — переход по ссылке из потока Power Automate или клик по
|
||||
// строке таблицы:
|
||||
// …/СтраницаПортала.aspx?rpmode=approve&rptype=<форма>&rpid=<RowID>
|
||||
@@ -22,7 +23,7 @@ import * as React from 'react';
|
||||
export type PortalMode = 'submit' | 'approve' | 'create';
|
||||
|
||||
/** Раздел бокового меню. */
|
||||
export type NavSection = 'new' | 'approvals' | 'own';
|
||||
export type NavSection = 'new' | 'approvals' | 'own' | 'search';
|
||||
|
||||
export interface IRoute {
|
||||
mode: PortalMode;
|
||||
@@ -39,7 +40,7 @@ export interface IRoute {
|
||||
const ROUTE_CHANGE_EVENT = 'rp-route-change';
|
||||
|
||||
function parseNav(raw: string | undefined): NavSection {
|
||||
if (raw === 'approvals' || raw === 'own') return raw;
|
||||
if (raw === 'approvals' || raw === 'own' || raw === 'search') return raw;
|
||||
return 'new';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// Текст поиска переживает перезагрузку страницы (localStorage), но не более
|
||||
// того — это просто «продолжить с того же места», а не синхронизация между
|
||||
// вкладками/устройствами. key должен быть уникальным на раздел (см. вызовы в
|
||||
// RequestsTableView/SearchView), иначе значения разных таблиц перепишут друг друга.
|
||||
function readStored(storageKey: string): string {
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function usePersistedQuery(key: string): [string, (value: string) => void] {
|
||||
const storageKey = `rp-search-query:${key}`;
|
||||
|
||||
const [value, setValue] = React.useState<string>(() => readStored(storageKey));
|
||||
|
||||
// Компонент может продолжать жить, когда key поменялся: RequestsTableView —
|
||||
// это один и тот же элемент для вкладок «На согласовании» и «Мои заявки»,
|
||||
// меняется только проп kind. Инициализатор useState при этом повторно НЕ
|
||||
// срабатывает, поэтому без перечитывания текст из одной вкладки оставался бы
|
||||
// на экране во второй и записывался уже под её ключ — выглядело бы так, будто
|
||||
// ключ у них общий.
|
||||
const prevKey = React.useRef<string>(storageKey);
|
||||
React.useEffect(() => {
|
||||
if (prevKey.current === storageKey) return;
|
||||
prevKey.current = storageKey;
|
||||
setValue(readStored(storageKey));
|
||||
}, [storageKey]);
|
||||
|
||||
const setPersisted = React.useCallback(
|
||||
(next: string): void => {
|
||||
setValue(next);
|
||||
try {
|
||||
if (next) {
|
||||
window.localStorage.setItem(storageKey, next);
|
||||
} else {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
}
|
||||
} catch {
|
||||
// localStorage недоступен (приватный режим и т.п.) — просто не сохраняем.
|
||||
}
|
||||
},
|
||||
[storageKey]
|
||||
);
|
||||
|
||||
return [value, setPersisted];
|
||||
}
|
||||
|
||||
// Задержка перед тем, как введённый текст уйдёт в запрос. Без неё поиск
|
||||
// срабатывает на КАЖДОЕ нажатие клавиши: «согласование» — это 12 полных циклов
|
||||
// (Search + дочитывание через REST по каждому типу заявок), из которых нужен
|
||||
// только последний. Заодно снимает часть нагрузки, из-за которой SharePoint
|
||||
// начинает throttling'ить (429).
|
||||
const SEARCH_DEBOUNCE_MS = 450;
|
||||
|
||||
// Значение, отстающее от переданного на SEARCH_DEBOUNCE_MS: в поле ввода текст
|
||||
// меняется сразу (без «залипания»), а в запрос уходит только когда пользователь
|
||||
// перестал печатать. Пустая строка проходит без задержки — очистка поиска
|
||||
// должна возвращать полный список мгновенно.
|
||||
export function useDebouncedQuery(value: string, delayMs: number = SEARCH_DEBOUNCE_MS): string {
|
||||
const [debounced, setDebounced] = React.useState<string>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!value) {
|
||||
setDebounced('');
|
||||
return undefined;
|
||||
}
|
||||
const timer = window.setTimeout(() => setDebounced(value), delayMs);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [value, delayMs]);
|
||||
|
||||
return debounced;
|
||||
}
|
||||
Reference in New Issue
Block a user