From bf706907ded1dd64e5f205a4d72fd2c52aeb8101 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 21 Aug 2026 09:45:20 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forms/Clearance/ClearanceForm.tsx | 3 ++- .../forms/Clearance/ClearanceView.tsx | 3 ++- .../RecruitingHire/RecruitingHireForm.tsx | 3 ++- .../RecruitingHire/RecruitingHireView.tsx | 3 ++- .../RecruitingSelectionForm.tsx | 3 ++- .../RecruitingSelectionView.tsx | 3 ++- .../forms/Ticket1C/Ticket1CForm.tsx | 3 ++- .../forms/Ticket1C/Ticket1CView.tsx | 3 ++- .../forms/TmcTransfer/TmcTransferForm.tsx | 3 ++- .../forms/TmcTransfer/TmcTransferView.tsx | 3 ++- .../forms/UnpaidLeave/UnpaidLeaveForm.tsx | 3 ++- .../forms/UnpaidLeave/UnpaidLeaveView.tsx | 3 ++- .../forms/Vacation/VacationForm.tsx | 3 ++- .../forms/Vacation/VacationView.tsx | 3 ++- .../WorkplaceSetup/WorkplaceSetupForm.tsx | 3 ++- .../WorkplaceSetup/WorkplaceSetupView.tsx | 3 ++- .../requestPortal/shared/diagnostics.ts | 25 +++++++++++++++++++ 17 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index 427739e..355b7b8 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -26,6 +26,7 @@ import { unassignedStepMessage } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -79,7 +80,7 @@ const ClearanceForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx index 4f041ee..83f41c8 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -13,6 +13,7 @@ import { useRequestTypeId, APPROVAL_PHASE } from '../../shared/approval'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -105,7 +106,7 @@ const ClearanceView: React.FC = ({ context, itemId, formKey }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index aa7cbb6..3dbf93e 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -36,6 +36,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { approvalListTitle, @@ -91,7 +92,7 @@ const RecruitingHireForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 0ce8804..5404997 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -15,6 +15,7 @@ import { } from '../../shared/approval'; import { navigate } from '../../shared/routing'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -117,7 +118,7 @@ const RecruitingHireView: React.FC = ({ context, itemI }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index b26b474..3f601c2 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -35,6 +35,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { approvalListTitle, @@ -88,7 +89,7 @@ const RecruitingSelectionForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx index 2882216..8eba89a 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx @@ -13,6 +13,7 @@ import { useRequestTypeId } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -83,7 +84,7 @@ const RecruitingSelectionView: React.FC = ({ cont }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 8850622..1ae6766 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -35,6 +35,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { approvalListTitle, @@ -105,7 +106,7 @@ const Ticket1CForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx index 1a7ef75..814f561 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx @@ -27,6 +27,7 @@ import { REQUEST_STATUS } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -122,7 +123,7 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey, }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index 0d90a43..2f82ad1 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -36,6 +36,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { approvalListTitle, @@ -104,7 +105,7 @@ const TmcTransferForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx index bd98fef..4722e8c 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx @@ -27,6 +27,7 @@ import { APPROVAL_PHASE } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -107,7 +108,7 @@ const TmcTransferView: React.FC = ({ context, itemId, for }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index ee15ae6..e75e03c 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -25,6 +25,7 @@ import { resolveWriteListId } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -78,7 +79,7 @@ const UnpaidLeaveForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx index 37413b5..a2abf3b 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx @@ -12,6 +12,7 @@ import { notifyActiveStep, useRequestTypeId } from '../../shared/approval'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -77,7 +78,7 @@ const UnpaidLeaveView: React.FC = ({ context, itemId, for }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index 738f0ad..a03926d 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -25,6 +25,7 @@ import { unassignedStepMessage } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -78,7 +79,7 @@ const VacationForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx index bad187a..114eb8d 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx @@ -12,6 +12,7 @@ import { notifyActiveStep, useRequestTypeId } from '../../shared/approval'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -77,7 +78,7 @@ const VacationView: React.FC = ({ context, itemId, formKey, }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 9c84956..49f43ff 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -36,6 +36,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { approvalListTitle, @@ -93,7 +94,7 @@ const WorkplaceSetupForm: React.FC = ({ }) .catch((e) => { if (alive) { - submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`); + submit.setError(`Не удалось загрузить заявку №${editId}: ${friendlyErrorMessage(e)}`); } }); return () => { diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index e7cd88b..08d5d62 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -27,6 +27,7 @@ import { APPROVAL_PHASE } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; +import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, approvalListTitleForListId, @@ -122,7 +123,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI }) .catch((e) => { if (alive) { - setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`); + setError(`Не удалось загрузить заявку №${itemId}: ${friendlyErrorMessage(e)}`); setLoading(false); } }); diff --git a/src/webparts/requestPortal/shared/diagnostics.ts b/src/webparts/requestPortal/shared/diagnostics.ts index 69a44e4..09457ac 100644 --- a/src/webparts/requestPortal/shared/diagnostics.ts +++ b/src/webparts/requestPortal/shared/diagnostics.ts @@ -52,6 +52,31 @@ export function describeReason(reason: unknown): string { } } +// PnPjs/SharePoint кладёт в Error.message текст ответа сервера целиком — +// например: `Error making HttpClient request in queryable [404] ::> +// {"odata.error":{"message":{"value":"Элемент не существует..."}}}`. Пока это +// строка, а не распарсенный JSON, русские буквы в ней остаются как литеральные +// escape-последовательности `Эл...`, и в UI вместо кириллицы видно +// набор из "\u". Достаём odata.error.message.value через JSON.parse — сам +// парсер раскодирует escape-последовательности обратно в текст. +export function friendlyErrorMessage(e: unknown): string { + const message = e instanceof Error ? e.message : String(e); + const start = message.indexOf('{'); + const end = message.lastIndexOf('}'); + if (start !== -1 && end > start) { + try { + const parsed = JSON.parse(message.slice(start, end + 1)) as { + 'odata.error'?: { message?: { value?: string } }; + }; + const value = parsed['odata.error']?.message?.value; + if (value) return value; + } catch { + // Тело не JSON (или не то, что ожидали) — отдаём исходное сообщение. + } + } + return message; +} + function logError(kind: string, reason: unknown): void { console.error(`${PREFIX} ${kind}: ${describeReason(reason)}`); // Стек и объект — отдельными аргументами: так консоль браузера даёт -- 2.34.1 From 98324b18811fbb7baec9faa920ae27fef43eb843 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 21 Aug 2026 11:28:04 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/webparts/requestPortal/shared/diagnostics.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/webparts/requestPortal/shared/diagnostics.ts b/src/webparts/requestPortal/shared/diagnostics.ts index 09457ac..8a4e068 100644 --- a/src/webparts/requestPortal/shared/diagnostics.ts +++ b/src/webparts/requestPortal/shared/diagnostics.ts @@ -59,6 +59,16 @@ export function describeReason(reason: unknown): string { // escape-последовательности `Эл...`, и в UI вместо кириллицы видно // набор из "\u". Достаём odata.error.message.value через JSON.parse — сам // парсер раскодирует escape-последовательности обратно в текст. +// SharePoint отдаёт этот же текст и когда элемент правда удалён, и когда у +// пользователя просто нет прав на список/элемент (сервер прячет 403 за 404, +// чтобы не подтверждать существование элемента посторонним). По одному +// тексту эти два случая не различить, а "нет доступа" на практике встречается +// куда чаще, чем совпадение по времени с удалением — поэтому подменяем +// формулировку на более вероятную причину. +const ITEM_NOT_FOUND = 'Элемент не существует. Возможно, он был удален другим пользователем.'; +const NO_ACCESS_HINT = + 'Элемент не найден. Скорее всего, у вас нет доступа к этому списку или элементу (либо он был удалён).'; + export function friendlyErrorMessage(e: unknown): string { const message = e instanceof Error ? e.message : String(e); const start = message.indexOf('{'); @@ -69,6 +79,7 @@ export function friendlyErrorMessage(e: unknown): string { 'odata.error'?: { message?: { value?: string } }; }; const value = parsed['odata.error']?.message?.value; + if (value === ITEM_NOT_FOUND) return NO_ACCESS_HINT; if (value) return value; } catch { // Тело не JSON (или не то, что ожидали) — отдаём исходное сообщение. -- 2.34.1