From 467ceb0d483c27dc9c664e2891fca2369d3bafcb Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Tue, 25 Aug 2026 14:04:57 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BD=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=20=D1=83=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D1=85=D0=B5=20=D1=80=D0=B0=D0=BD=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=E2=80=94=20=D0=BD?= =?UTF-8?q?=D0=B8=20=D0=BE=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B8,?= =?UTF-8?q?=20=D0=BD=D0=B8=20=D0=BE=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=BC?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) DecisionPanel ждёт onDecided, если тот вернул Promise: «Решение сохранено. Статус заявки: «Согласовано»» появляется только после побочных действий решения — для заявления на увольнение это созданный обходной лист. Пока идёт создание, кнопки остаются заблокированными, а на месте результата — «Создаём обходной лист…». Провал побочного действия статус решения не отменяет (оно уже применено на сервере) — о проблеме сообщает сам обработчик своим предупреждением. 2) «Вы ещё не создавали заявок» больше не мигает на недогруженном списке. «Мои заявки» и «Доработка» собираются из двух источников (Search + REST-довесок по свежести), и onPartial показывал тот, что ответил первым, даже если он пустой — на экране на мгновение появлялась пустота вместо «Загрузка…». Теперь пустой промежуточный результат не показывается вовсе, а вывод «заявок нет» делается только по ЗАВЕРШЁННОЙ загрузке именно этой таблицы (loadedKey), а не по любому пустому rows. Co-Authored-By: Claude Opus 5 --- .../features/myRequests/RequestsTableView.tsx | 22 ++++++++++++-- .../forms/Dismissal/DismissalView.tsx | 20 +++++++------ .../shared/approval/ui/DecisionPanel.tsx | 30 +++++++++++++++---- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx index a8a139c..bab5da9 100644 --- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx +++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx @@ -421,6 +421,12 @@ export const RequestsTableView: React.FC = ({ ? undefined : (partial: IPage): void => { if (cancelled) return; + // ПУСТОЙ промежуточный результат не показываем: смысл onPartial — + // не держать экран пустым, пока отвечает более медленный источник, + // а пустая страница как раз пустой экран и даёт, только вместо + // «Загрузка…» на нём появляется «Вы ещё не создавали заявок» — + // и через мгновение исчезает, когда придёт второй источник. + if (partial.rows.length === 0) return; setRows(partial.rows); setTotalRows(partial.totalRows); }; @@ -477,6 +483,18 @@ export const RequestsTableView: React.FC = ({ const q = debouncedQuery.trim(); const total = totalRows; const visible = rows || []; + + /** + * Можно ли уже судить о содержимом таблицы. + * + * Пустоту («Вы ещё не создавали заявок») объявляем ТОЛЬКО по завершённой + * загрузке именно ЭТОЙ таблицы (loadedKey), а не по любому пустому rows: до + * этого rows может быть пустым по совершенно другой причине — часть источников + * ещё в пути (см. onPartial выше, «Мои заявки» собираются из Search + REST). + * Промежуточный НЕПУСТОЙ результат показывать можно и нужно — в нём уже есть + * что смотреть. + */ + const loaded = loadedKey === dataKey || visible.length > 0; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); // Страница может оказаться за пределами набора: пришла из старой ссылки или @@ -527,8 +545,8 @@ export const RequestsTableView: React.FC = ({ {error &&
Не удалось загрузить заявки: {error}
} - {!error && rows === undefined &&
Загрузка…
} - {!error && rows && total === 0 && ( + {!error && !loaded &&
Загрузка…
} + {!error && loaded && total === 0 && (
{q ? `По запросу «${debouncedQuery}» ничего не найдено.` : EMPTY_TEXTS[view]}
diff --git a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx index 976cc51..1d6dead 100644 --- a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx @@ -121,9 +121,10 @@ const DismissalView: React.FC = ({ context, itemId, formKey // выводятся из заявления, заполнять его руками нечего — см. createClearance.ts). // Свой статус решения при этом уже применён: неудача автосоздания не отменяет // согласование, поэтому она показывается предупреждением, а не ошибкой заявки. - const createClearance = (): void => { + const createClearance = (): Promise => { setClearanceError(''); - ensureClearanceForDismissal(context, itemId, listId) + setClearance('Создаём обходной лист по этому заявлению…'); + return ensureClearanceForDismissal(context, itemId, listId) .then((result) => { setClearance( result.existing @@ -135,11 +136,12 @@ const DismissalView: React.FC = ({ context, itemId, formKey // только после перезагрузки страницы. setData((prev) => ({ ...prev, ClearanceUrl: result.url })); }) - .catch((e) => + .catch((e) => { + setClearance(''); setClearanceError( `Заявление согласовано, но обходной лист создать не удалось: ${friendlyErrorMessage(e)} — заведите его вручную.` - ) - ); + ); + }); }; return ( @@ -185,10 +187,10 @@ const DismissalView: React.FC = ({ context, itemId, formKey notify(); // «Согласовано» = маршрут заявления пройден целиком (фазы исполнения у // него нет, см. README формы) — значит увольнение подтверждено и пора - // заводить обходной лист. - if (newStatus === REQUEST_STATUS.approved) { - createClearance(); - } + // заводить обходной лист. Промис ВОЗВРАЩАЕМ: пока он не разрешится, + // панель решения не пишет «Решение сохранено» (см. её onDecided) — + // иначе успех появлялся бы раньше, чем обходной лист создан. + return newStatus === REQUEST_STATUS.approved ? createClearance() : undefined; }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/shared/approval/ui/DecisionPanel.tsx b/src/webparts/requestPortal/shared/approval/ui/DecisionPanel.tsx index 4615937..b26423e 100644 --- a/src/webparts/requestPortal/shared/approval/ui/DecisionPanel.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/DecisionPanel.tsx @@ -11,8 +11,20 @@ interface IDecisionPanelProps { service: ApproverService; /** Id заявки (элемент списка данных). */ requestId: number; - /** Уведомление после сохранения решения (например, обновить статус на экране). */ - onDecided?: (status: string) => void; + /** + * Уведомление после сохранения решения (например, обновить статус на экране). + * + * Может вернуть Promise — тогда панель ДОЖДЁТСЯ его и только потом покажет + * «Решение сохранено». Так работают побочные действия решения, без которых + * ответ «согласовано» неполон: по согласованному заявлению на увольнение + * автоматически создаётся обходной лист (см. forms/Dismissal), и сообщать об + * успехе, пока он не создан, — значит показывать успех раньше времени. + * + * Отказ (reject) панель НЕ считает провалом решения — решение к этому моменту + * уже применено на сервере; сообщить о проблеме побочного действия должен сам + * обработчик (у него для этого есть своя область экрана). + */ + onDecided?: (status: string) => void | Promise; /** Выполнить ПЕРЕД применением решения (напр. сохранить поля, заполненные * согласующим). Если бросит — решение не применяется, показывается ошибка. */ onBeforeDecide?: () => Promise; @@ -83,11 +95,19 @@ export const DecisionPanel: React.FC = ({ await onBeforeDecide(); } const status = await service.decide(requestId, pendingRow.id, decision, comment.trim()); + // Сначала побочные действия решения, потом «сохранено»: пока onDecided не + // отработал, кнопки остаются заблокированными (submitting), и пользователь не + // видит успеха раньше, чем всё сделано. Провал onDecided на статус решения не + // влияет — оно уже применено, поэтому сообщение показываем всё равно. + if (onDecided) { + try { + await onDecided(status); + } catch (e) { + console.error('Побочное действие после решения не выполнилось', e); + } + } setResultStatus(status); setSubmitting(false); - if (onDecided) { - onDecided(status); - } } catch (e) { setError(`Не удалось сохранить решение: ${(e as Error).message}`); setSubmitting(false);