diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index 54e022f..5980aa5 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -141,6 +141,10 @@ const RecruitingHireForm: React.FC = ({ requestType, context, position: r.position, allowReassign: r.allowReassign, order: r.order, + // Транзитом из маршрута в строку списка: наследование бэкенд применил + // при resolve, форма только переносит значения (см. IMatrixRow.slaHours). + slaHours: r.slaHours, + askOverdueReason: r.askOverdueReason, })); // Удаление уже прикреплённого файла (в правке) — сразу из заявки. diff --git a/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json index 5473480..26dd879 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json @@ -9,6 +9,8 @@ { "internalName": "StepOrder", "title": "Порядок", "type": "Number" }, { "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }, { "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }, + { "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" }, + { "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" }, { "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" } ] } diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 859e050..07b14d2 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -117,7 +117,11 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con email: r.email, position: r.position, allowReassign: r.allowReassign, - order: r.order + order: r.order, + // Транзитом из маршрута в строку списка: наследование бэкенд применил + // при resolve, форма только переносит значения (см. IMatrixRow.slaHours). + slaHours: r.slaHours, + askOverdueReason: r.askOverdueReason })); const handleRemoveAttachment = (fileName: string): void => { diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/approval/provisioning.json b/src/webparts/requestPortal/forms/RecruitingSelection/approval/provisioning.json index 0c56450..9707088 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingSelection/approval/provisioning.json @@ -9,6 +9,8 @@ { "internalName": "StepOrder", "title": "Порядок", "type": "Number" }, { "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }, { "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }, + { "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" }, + { "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" }, { "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" } ] } diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 31484ad..a474731 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -172,7 +172,11 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan email: r.email, position: r.position, allowReassign: r.allowReassign, - order: r.order + order: r.order, + // Транзитом из маршрута в строку списка: наследование бэкенд применил + // при resolve, форма только переносит значения (см. IMatrixRow.slaHours). + slaHours: r.slaHours, + askOverdueReason: r.askOverdueReason })); const handleRemoveAttachment = (fileName: string): void => { diff --git a/src/webparts/requestPortal/forms/Ticket1C/approval/provisioning.json b/src/webparts/requestPortal/forms/Ticket1C/approval/provisioning.json index 3da3e45..d144c07 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/Ticket1C/approval/provisioning.json @@ -9,6 +9,8 @@ { "internalName": "StepOrder", "title": "Порядок", "type": "Number" }, { "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }, { "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }, + { "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" }, + { "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" }, { "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" } ] } diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index 51c4cf0..ba9fd0e 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -138,7 +138,11 @@ const VacationForm: React.FC = ({ requestType, context, onCan email: r.email, position: r.position, allowReassign: r.allowReassign, - order: r.order + order: r.order, + // Транзитом из маршрута в строку списка: наследование бэкенд применил + // при resolve, форма только переносит значения (см. IMatrixRow.slaHours). + slaHours: r.slaHours, + askOverdueReason: r.askOverdueReason })); const save = (status: string): (() => Promise) => async (): Promise => { diff --git a/src/webparts/requestPortal/forms/Vacation/approval/provisioning.json b/src/webparts/requestPortal/forms/Vacation/approval/provisioning.json index 557a7d6..6558228 100644 --- a/src/webparts/requestPortal/forms/Vacation/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/Vacation/approval/provisioning.json @@ -9,6 +9,8 @@ { "internalName": "StepOrder", "title": "Порядок", "type": "Number" }, { "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }, { "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }, + { "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" }, + { "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" }, { "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" } ] } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 58271d1..e765f6d 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -164,6 +164,10 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, position: r.position, allowReassign: r.allowReassign, order: r.order, + // Транзитом из маршрута в строку списка: наследование бэкенд применил + // при resolve, форма только переносит значения (см. IMatrixRow.slaHours). + slaHours: r.slaHours, + askOverdueReason: r.askOverdueReason, })); // Удаление уже прикреплённого файла (в правке) — сразу из заявки. diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/approval/provisioning.json b/src/webparts/requestPortal/forms/WorkplaceSetup/approval/provisioning.json index 09fd21e..f6b867d 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/approval/provisioning.json @@ -9,6 +9,8 @@ { "internalName": "StepOrder", "title": "Порядок", "type": "Number" }, { "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }, { "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }, + { "internalName": "SLAhours", "title": "SLA, часов", "type": "Number" }, + { "internalName": "AskOverdueReason", "title": "Опрос причины просрочки", "type": "Boolean" }, { "internalName": "DelegatedBy", "title": "Кем переназначено", "type": "User", "userSelectionMode": "PeopleOnly" } ] } diff --git a/src/webparts/requestPortal/shared/approval/approvalList.service.ts b/src/webparts/requestPortal/shared/approval/approvalList.service.ts index 09533ad..5535a17 100644 --- a/src/webparts/requestPortal/shared/approval/approvalList.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalList.service.ts @@ -32,6 +32,8 @@ const FIELDS = { allowReassign: 'AllowReassignToOtherApprovers', // «Можно перепоручать» (Да/Нет) phase: 'Phase', // «Фаза» (Choice): Согласование | Исполнение delegatedBy: 'DelegatedBy', // «Кем переназначено» (User) → DelegatedById + slaHours: 'SLAhours', // «SLA, часов» (Number) — пусто значит «взять у типа заявки» + askOverdueReason: 'AskOverdueReason', // «Опрос причины просрочки» (Да/Нет) }; // Согласующий для записи в список. @@ -45,6 +47,18 @@ export interface IApproverToWrite { * (ApprovalMatrix/useMatrixEditor) всегда даёт уникальный order по позиции — * это поле для параллельных шагов, приходящих из внешнего маршрута напрямую. */ order?: number; + /** + * SLA шага в часах и признак опроса причины просрочки — приходят из resolve + * с уже применённым наследованием «шаг перекрывает тип заявки» (см. бэкенд + * ResolveStepAsync). Портал их не вычисляет, только переносит в строку. + * + * undefined/null — «в строке не задано»: тогда напоминания используют SLA + * типа заявки. Ноль — это НЕ пусто, а «напоминать сразу». + */ + // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано» (в SharePoint пустая колонка) + slaHours?: number | null; + // eslint-disable-next-line @rushstack/no-new-null -- см. slaHours + askOverdueReason?: boolean | null; } // Все запросы к списку «<форма> — согласование». listTitle задаётся формой. @@ -101,6 +115,9 @@ export class ApprovalListService { [FIELDS.order]: order, [FIELDS.allowReassign]: a.allowReassign, [FIELDS.phase]: phase, + // ?? null, а не || null: ноль часов — значимое значение. + [FIELDS.slaHours]: a.slaHours ?? null, + [FIELDS.askOverdueReason]: a.askOverdueReason ?? null, }); } } @@ -222,6 +239,8 @@ export class ApprovalListService { FIELDS.order, FIELDS.allowReassign, FIELDS.phase, + FIELDS.slaHours, + FIELDS.askOverdueReason, `${FIELDS.approver}/Title`, `${FIELDS.approver}/EMail` ) @@ -240,6 +259,11 @@ export class ApprovalListService { comment: (r[FIELDS.comment] as string) || '', allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false, phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval, + // Читаем, чтобы не потерять при пересохранении: writeRoute удаляет + // строки и создаёт их заново из матрицы, и без этих полей SLA, + // проставленный при первой отправке, обнулился бы у черновика. + slaHours: (r[FIELDS.slaHours] as number | null) ?? null, + askOverdueReason: (r[FIELDS.askOverdueReason] as boolean | null) ?? null, }; }); } diff --git a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts index df994b4..47df5c2 100644 --- a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts @@ -33,6 +33,8 @@ export async function getPlannedMatrix( comment: '', allowReassign: s.allowReassignToOtherApprovers, phase, + slaHours: s.slaHours, + askOverdueReason: s.askOverdueReason, })) ); } diff --git a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts index e8d98d8..6dcd954 100644 --- a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts +++ b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts @@ -47,4 +47,12 @@ export interface IResolvedRouteStep { // Предупреждение маршрута по этому шагу (например, роль не разрешилась в // конкретного человека) — приходит от API, показываем автору при желании. warning: string | null; + // SLA шага в часах и признак опроса причины просрочки. Наследование + // «шаг перекрывает тип заявки» бэкенд уже применил — здесь итоговое значение, + // его достаточно записать в строку списка согласования. + // null — SLA не настроен, напоминаний по шагу не будет. + // eslint-disable-next-line @rushstack/no-new-null -- зеркалит nullable-контракт бэкенда + slaHours: number | null; + // eslint-disable-next-line @rushstack/no-new-null -- см. slaHours + askOverdueReason: boolean | null; } diff --git a/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts b/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts index 5ebc0c1..3cd42da 100644 --- a/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts +++ b/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts @@ -15,4 +15,16 @@ export interface IMatrixRow { // редактировании — переключается кнопкой и заново раздаёт order (см. // useMatrixEditor/withOrders). parallelWithPrev?: boolean; + /** + * SLA шага и признак опроса причины просрочки. Матрица здесь — транзитное + * звено: значения приходят из resolve (или читаются из уже записанных строк) + * и уходят обратно в список при сохранении. На форме они не показываются и + * не редактируются — правятся в админке. + * + * null/undefined — «не задано», работает SLA типа заявки. Ноль — «сразу». + */ + // eslint-disable-next-line @rushstack/no-new-null -- null приходит из SharePoint и значит «не задано» + slaHours?: number | null; + // eslint-disable-next-line @rushstack/no-new-null -- см. slaHours + askOverdueReason?: boolean | null; } diff --git a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx index f9d7778..e32b30f 100644 --- a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx @@ -79,6 +79,12 @@ export const ApprovalMatrix: React.FC = ({ // Пустые колонки не показываем — столбец из одних прочерков только шумит. const hasStatuses = rows.some((r) => r.status && r.status !== '—'); const hasDates = rows.some((r) => !!r.date); + // slaHours на строке — уже ИТОГОВОЕ значение с применённым наследованием + // (шаг перекрывает тип заявки): для черновика его так отдаёт resolve + // (getPlannedMatrix), для поданной заявки — то, что записали в строку при + // отправке (см. writeRoute). Здесь его достаточно просто показать, повторно + // резолвить нечего. + const hasSla = rows.some((r) => r.slaHours !== undefined && r.slaHours !== null); // Число колонок (для colSpan) — на случай будущих нужд не используется: комментарий // выводим внутри ячейки согласующего, чтобы не ломать rowSpan колонки «Шаг». @@ -101,6 +107,7 @@ export const ApprovalMatrix: React.FC = ({ Должность {hasStatuses && Статус} {hasDates && Дата} + {hasSla && SLA} {editable && } {editable && } @@ -137,6 +144,9 @@ export const ApprovalMatrix: React.FC = ({ )} {hasDates && {r.date}} + {hasSla && ( + {r.slaHours || r.slaHours === 0 ? `${r.slaHours} ч` : '—'} + )} {/* Перемещение действует на ШАГ целиком (параллельные строки переезжают вместе): одна пара стрелок на весь шаг. */} {editable && isBlockStart && ( diff --git a/src/webparts/requestPortal/shared/diagnostics.ts b/src/webparts/requestPortal/shared/diagnostics.ts index 58552bb..69a44e4 100644 --- a/src/webparts/requestPortal/shared/diagnostics.ts +++ b/src/webparts/requestPortal/shared/diagnostics.ts @@ -52,16 +52,26 @@ export function describeReason(reason: unknown): string { } } +function logError(kind: string, reason: unknown): void { + console.error(`${PREFIX} ${kind}: ${describeReason(reason)}`); + // Стек и объект — отдельными аргументами: так консоль браузера даёт + // раскрыть объект и перейти по стеку, а не показывает «[object Object]». + if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack); + console.error(`${PREFIX} Причина целиком:`, reason); +} + export function installErrorLogging(): void { if (installed) return; installed = true; window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => { - const reason: unknown = e.reason; - console.error(`${PREFIX} Необработанная ошибка: ${describeReason(reason)}`); - // Стек и объект — отдельными аргументами: так консоль браузера даёт - // раскрыть объект и перейти по стеку, а не показывает «[object Object]». - if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack); - console.error(`${PREFIX} Причина целиком:`, reason); + logError('Необработанная ошибка', e.reason); + }); + + // Синхронные ошибки страницы. Раньше их ловил ErrorOverlay ради углового + // уведомления; уведомление убрано (чужая ошибка со страницы SharePoint + // выглядела как поломка портала), а разбор нужен — поэтому он переехал сюда. + window.addEventListener('error', (e: ErrorEvent) => { + logError('Ошибка на странице', e.error || e.message); }); } diff --git a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss index 8563c27..97dc0c7 100644 --- a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss +++ b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss @@ -83,73 +83,3 @@ border-color: $gold-400; } } - -// Уведомление об АСИНХРОННОЙ ошибке: в углу, поверх содержимого, но ничего не -// перекрывает и не блокирует — портал остаётся рабочим (см. ErrorOverlay.tsx о -// том, почему такие ошибки больше не схлопывают вебпарт). -.toast { - position: fixed; - right: $sp-4; - bottom: $sp-4; - z-index: 999998; - box-sizing: border-box; - width: 360px; - max-width: calc(100vw - #{$sp-4} * 2); - padding: $sp-3 $sp-4; - border-radius: $r-md; - border-left: 4px solid $red-500; - background: $white; - box-shadow: $sh-modal; - font-family: $font-sans; -} - -.toastHead { - display: flex; - align-items: center; - justify-content: space-between; - gap: $sp-2; -} - -.toastTitle { - font-size: $fs-sm; - font-weight: 600; - color: $navy-950; -} - -.toastClose { - padding: 0 4px; - border: 0; - background: none; - color: $gray-500; - font-size: 18px; - line-height: 1; - cursor: pointer; - - &:hover { - color: $navy-800; - } -} - -.toastText { - margin-top: $sp-1; - max-height: 96px; - overflow: auto; - font-size: $fs-caption; - color: $gray-900; - word-break: break-word; -} - -.toastCopy { - margin-top: $sp-2; - padding: 4px 10px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - color: $navy-800; - font-size: $fs-caption; - cursor: pointer; - - &:hover { - border-color: $gold-400; - } -} diff --git a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx index e7a9b35..e01f753 100644 --- a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx +++ b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx @@ -5,14 +5,12 @@ import styles from './ErrorOverlay.module.scss'; interface ICaughtError { message: string; stack: string; - source: string; // откуда поймана: React-рендер, window.onerror, unhandledrejection + source: string; } interface IErrorOverlayState { /** Падение самого рендера — дерево сломано, показывать нечего. */ fatal: ICaughtError | undefined; - /** Асинхронная ошибка со страницы — портал работает, это просто уведомление. */ - notice: ICaughtError | undefined; } function formatError(err: unknown, source: string): ICaughtError { @@ -24,7 +22,7 @@ function formatError(err: unknown, source: string): ICaughtError { // at vp (request-portal-web-part_.js:9:397946) // at t.onUnhandledRejection (...) // - // Место, где промис реально отклонился, при этом терялось полностью. + // Место, где ошибка реально возникла, при этом терялось полностью. if (err instanceof Error) { return { message: err.message || describeReason(err), @@ -41,112 +39,46 @@ function copyToClipboard(text: string): void { navigator.clipboard.writeText(text).catch(() => undefined); } -function fullTextOf(error: ICaughtError): string { - return `[${error.source}] ${error.message}\n\n${error.stack}`; -} - /** - * Ловит падения, которые иначе схлопывают вебпарт в SPFx workbench. + * Граница ошибок React: ловит падение РЕНДЕРА, из-за которого вебпарт иначе + * схлопывается в пустое место, и показывает вместо него текст ошибки со стеком. * - * Разделение на fatal и notice — не косметика. Слушатели `error` и - * `unhandledrejection` вешаются на window, то есть срабатывают на ВСЮ страницу - * SharePoint: на другие вебпарты, на скрипты самого SharePoint, на любые - * фоновые промисы. Раньше любое такое отклонение — даже постороннее и - * безобидное — заменяло собой весь портал: вместо открытой заявки пользователь - * получал экран «Необработанное исключение». Именно так это и проявлялось при - * переходе по ссылке на заявку. - * - * Теперь портал заменяется только при падении РЕНДЕРА (там показывать - * действительно нечего), а асинхронные ошибки — угловое уведомление, которое - * можно закрыть; работать оно не мешает. Полный разбор причины при этом всё - * равно уходит в консоль — см. shared/diagnostics.ts. + * Асинхронные ошибки страницы (`unhandledrejection`, `window.onerror`) здесь + * НЕ ловятся намеренно. Слушатели на window срабатывают на всю страницу + * SharePoint — на другие вебпарты, на скрипты самого SharePoint, на любые + * фоновые промисы, — и показывать по ним что-то пользователю смысла нет: портал + * при этом работает, а чужая ошибка выглядит как поломка портала. Разбор таких + * ошибок уходит в консоль, см. shared/diagnostics.ts. */ export class ErrorOverlay extends React.Component, IErrorOverlayState> { - public state: IErrorOverlayState = { fatal: undefined, notice: undefined }; + public state: IErrorOverlayState = { fatal: undefined }; - private onWindowError = (event: ErrorEvent): void => { - this.showNotice(formatError(event.error || event.message, 'window.onerror')); - }; - - private onUnhandledRejection = (event: PromiseRejectionEvent): void => { - this.showNotice(formatError(event.reason, 'unhandledrejection')); - }; - - private showNotice(notice: ICaughtError): void { - // Первое уведомление не затираем: за одной ошибкой обычно летит несколько - // производных, а разбираться нужно с исходной. - this.setState((prev) => (prev.notice ? prev : { ...prev, notice })); - } - - public componentDidMount(): void { - window.addEventListener('error', this.onWindowError); - window.addEventListener('unhandledrejection', this.onUnhandledRejection); - } - - public componentWillUnmount(): void { - window.removeEventListener('error', this.onWindowError); - window.removeEventListener('unhandledrejection', this.onUnhandledRejection); - } - - public static getDerivedStateFromError(err: unknown): Partial { + public static getDerivedStateFromError(err: unknown): IErrorOverlayState { return { fatal: formatError(err, 'React render') }; } public render(): React.ReactNode { - const { fatal, notice } = this.state; + const { fatal } = this.state; + if (!fatal) return this.props.children; - if (fatal) { - const fullText = fullTextOf(fatal); - return ( -
-
-
Необработанное исключение
-
{fatal.source}
-
{fullText}
-
- - -
-
-
- ); - } + const fullText = `[${fatal.source}] ${fatal.message}\n\n${fatal.stack}`; return ( - <> - {this.props.children} - {notice && ( -
-
- Что-то пошло не так - -
-
{notice.message}
- +
- )} - + + ); } }