fix: не сообщать об успехе раньше времени — ни о решении, ни о пустом списке

1) DecisionPanel ждёт onDecided, если тот вернул Promise: «Решение сохранено.
Статус заявки: «Согласовано»» появляется только после побочных действий решения —
для заявления на увольнение это созданный обходной лист. Пока идёт создание,
кнопки остаются заблокированными, а на месте результата — «Создаём обходной
лист…». Провал побочного действия статус решения не отменяет (оно уже применено на
сервере) — о проблеме сообщает сам обработчик своим предупреждением.

2) «Вы ещё не создавали заявок» больше не мигает на недогруженном списке.
«Мои заявки» и «Доработка» собираются из двух источников (Search + REST-довесок по
свежести), и onPartial показывал тот, что ответил первым, даже если он пустой — на
экране на мгновение появлялась пустота вместо «Загрузка…». Теперь пустой
промежуточный результат не показывается вовсе, а вывод «заявок нет» делается только
по ЗАВЕРШЁННОЙ загрузке именно этой таблицы (loadedKey), а не по любому пустому rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-08-25 14:04:57 +05:00
parent b3dee2047c
commit 467ceb0d48
3 changed files with 56 additions and 16 deletions
@@ -421,6 +421,12 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
? 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<IRequestsTableViewProps> = ({
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<IRequestsTableViewProps> = ({
</div>
{error && <div className={styles.error}>Не удалось загрузить заявки: {error}</div>}
{!error && rows === undefined && <div className={styles.empty}>Загрузка…</div>}
{!error && rows && total === 0 && (
{!error && !loaded && <div className={styles.empty}>Загрузка…</div>}
{!error && loaded && total === 0 && (
<div className={styles.empty}>
{q ? `По запросу «${debouncedQuery}» ничего не найдено.` : EMPTY_TEXTS[view]}
</div>
@@ -121,9 +121,10 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
// выводятся из заявления, заполнять его руками нечего — см. createClearance.ts).
// Свой статус решения при этом уже применён: неудача автосоздания не отменяет
// согласование, поэтому она показывается предупреждением, а не ошибкой заявки.
const createClearance = (): void => {
const createClearance = (): Promise<void> => {
setClearanceError('');
ensureClearanceForDismissal(context, itemId, listId)
setClearance('Создаём обходной лист по этому заявлению…');
return ensureClearanceForDismissal(context, itemId, listId)
.then((result) => {
setClearance(
result.existing
@@ -135,11 +136,12 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ 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<IDismissalViewProps> = ({ context, itemId, formKey
notify();
// «Согласовано» = маршрут заявления пройден целиком (фазы исполнения у
// него нет, см. README формы) — значит увольнение подтверждено и пора
// заводить обходной лист.
if (newStatus === REQUEST_STATUS.approved) {
createClearance();
}
// заводить обходной лист. Промис ВОЗВРАЩАЕМ: пока он не разрешится,
// панель решения не пишет «Решение сохранено» (см. её onDecided) —
// иначе успех появлялся бы раньше, чем обходной лист создан.
return newStatus === REQUEST_STATUS.approved ? createClearance() : undefined;
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -11,8 +11,20 @@ interface IDecisionPanelProps {
service: ApproverService;
/** Id заявки (элемент списка данных). */
requestId: number;
/** Уведомление после сохранения решения (например, обновить статус на экране). */
onDecided?: (status: string) => void;
/**
* Уведомление после сохранения решения (например, обновить статус на экране).
*
* Может вернуть Promise — тогда панель ДОЖДЁТСЯ его и только потом покажет
* «Решение сохранено». Так работают побочные действия решения, без которых
* ответ «согласовано» неполон: по согласованному заявлению на увольнение
* автоматически создаётся обходной лист (см. forms/Dismissal), и сообщать об
* успехе, пока он не создан, — значит показывать успех раньше времени.
*
* Отказ (reject) панель НЕ считает провалом решения — решение к этому моменту
* уже применено на сервере; сообщить о проблеме побочного действия должен сам
* обработчик (у него для этого есть своя область экрана).
*/
onDecided?: (status: string) => void | Promise<void>;
/** Выполнить ПЕРЕД применением решения (напр. сохранить поля, заполненные
* согласующим). Если бросит — решение не применяется, показывается ошибка. */
onBeforeDecide?: () => Promise<void>;
@@ -83,11 +95,19 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
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);