From 872686e855c9501be9f76a5d5d888d9535e766fe Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 16 Oct 2026 10:02:40 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=B5=20=D0=B8=D0=B7=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D1=8B=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=91=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FormModal получил isDirty: если в форме редактирования существующей записи что-то изменили, крестик, «Отмена» и клик мимо окна сначала показывают диалог «Закрыть без сохранения?». Подключено к редактированию типа заявки, маршрута, шага маршрута и согласующего в заявке. Формы создания не затронуты — их черновик и так сохраняется. Co-Authored-By: Claude Sonnet 5.5 --- .../screens/ManageRequestsApproverModal.tsx | 7 +- .../components/screens/RequestTypesScreen.tsx | 24 +++--- .../components/screens/RouteFormModal.tsx | 1 + .../components/screens/RouteStepFormModal.tsx | 1 + .../components/ui/ConfirmDialog.tsx | 5 +- .../approvalAdmin/components/ui/FormModal.tsx | 74 +++++++++++++------ frontend/src/webparts/approvalAdmin/texts.ts | 5 ++ 7 files changed, 84 insertions(+), 33 deletions(-) diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx index 6a6c119..ec19f29 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx @@ -74,6 +74,11 @@ export const ManageRequestsApproverModal: React.FC e.toLowerCase())); const selectableEmployees = employees.filter(e => !excludedSet.has(e.email.toLowerCase())); + const isDirty = + email !== initialEmail || + slaHours !== slaHoursToInput(initialSlaHours) || + askOverdueReason !== boolToTriState(initialAskOverdueReason); + const handleSubmit = (): void => { const trimmedEmail = email.trim(); if (!trimmedEmail) { @@ -95,7 +100,7 @@ export const ManageRequestsApproverModal: React.FC + = ({ api, sh const draft = useFormDraft(CREATE_DRAFT_STORAGE_KEY, EMPTY_FORM); const [editForm, setEditForm] = React.useState(EMPTY_FORM); const form = editing ? editForm : draft.value; + const isEditDirty = Boolean(editing) && JSON.stringify(editForm) !== JSON.stringify(toFormState(editing as RequestType)); const setForm = editing ? setEditForm : draft.setValue; const submit = useAsyncSubmit(); const sharePointLists = useSharePointLists(sharePoint, form.siteUrl); @@ -104,15 +117,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh const openEdit = (item: RequestType): void => { setEditing(item); - setEditForm({ - name: item.name, - pageUrl: item.pageUrl, - section: item.section, - siteUrl: item.siteUrl ?? '', - listId: item.listId ?? '', - slaHours: slaHoursToInput(item.slaHours), - askOverdueReason: boolToTriState(item.askOverdueReason) - }); + setEditForm(toFormState(item)); setShowForm(true); }; @@ -208,6 +213,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh onClose={() => setShowForm(false)} onReset={editing ? undefined : draft.reset} canReset={draft.isDirty} + isDirty={isEditDirty} > diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RouteFormModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RouteFormModal.tsx index 0e74cb8..b98c5d9 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RouteFormModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RouteFormModal.tsx @@ -116,6 +116,7 @@ export const RouteFormModal: React.FC = ({ onClose={onClose} onReset={editing ? undefined : draft.reset} canReset={draft.isDirty} + isDirty={Boolean(editing) && draft.isDirty} > diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx index f73b7d6..6766944 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx @@ -423,6 +423,7 @@ export const RouteStepFormModal: React.FC = ({ onClose={onClose} onReset={editing ? undefined : draft.reset} canReset={draft.isDirty} + isDirty={Boolean(editing) && draft.isDirty} > diff --git a/frontend/src/webparts/approvalAdmin/components/ui/ConfirmDialog.tsx b/frontend/src/webparts/approvalAdmin/components/ui/ConfirmDialog.tsx index 57d1f03..289f13d 100644 --- a/frontend/src/webparts/approvalAdmin/components/ui/ConfirmDialog.tsx +++ b/frontend/src/webparts/approvalAdmin/components/ui/ConfirmDialog.tsx @@ -8,6 +8,8 @@ export interface IConfirmDialogProps { title: string; text: string; confirmLabel: string; + /** Подпись кнопки отказа — по умолчанию «Отмена». */ + cancelLabel?: string; onConfirm: () => void; onCancel: () => void; busy?: boolean; @@ -17,6 +19,7 @@ export const ConfirmDialog: React.FC = ({ title, text, confirmLabel, + cancelLabel = texts.common.cancel, onConfirm, onCancel, busy @@ -26,7 +29,7 @@ export const ConfirmDialog: React.FC = ({
{title}

{text}

- +
diff --git a/frontend/src/webparts/approvalAdmin/components/ui/FormModal.tsx b/frontend/src/webparts/approvalAdmin/components/ui/FormModal.tsx index 6466e90..e1cc175 100644 --- a/frontend/src/webparts/approvalAdmin/components/ui/FormModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/ui/FormModal.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Alert } from './Alert'; import { Button } from './Button'; +import { ConfirmDialog } from './ConfirmDialog'; import { Modal } from './Modal'; import styles from './Modal.module.scss'; import { texts } from '../../texts'; @@ -22,6 +23,12 @@ export interface IFormModalProps { */ onReset?: () => void; canReset?: boolean; + /** + * В форме есть несохранённые правки существующей записи: выход (крестик, + * «Отмена», клик мимо окна) сначала спрашивает подтверждение. Для форм создания + * не нужен — их черновик сохраняется сам (см. useFormDraft). + */ + isDirty?: boolean; children: React.ReactNode; } @@ -39,26 +46,49 @@ export const FormModal: React.FC = ({ onClose, onReset, canReset, + isDirty, children -}) => ( - - {onReset && canReset && ( - - - - )} - - - - } - > - {error && {error}} - {children} - -); +}) => { + const [confirmingClose, setConfirmingClose] = React.useState(false); + // Во время сохранения не закрываем и не спрашиваем: запрос уже ушёл. + const requestClose = (): void => { + if (isSaving) return; + if (isDirty) setConfirmingClose(true); + else onClose(); + }; + + return ( + <> + + {onReset && canReset && ( + + + + )} + + + + } + > + {error && {error}} + {children} + + {confirmingClose && ( + setConfirmingClose(false)} + /> + )} + + ); +}; diff --git a/frontend/src/webparts/approvalAdmin/texts.ts b/frontend/src/webparts/approvalAdmin/texts.ts index 4fbbe43..df91bcb 100644 --- a/frontend/src/webparts/approvalAdmin/texts.ts +++ b/frontend/src/webparts/approvalAdmin/texts.ts @@ -78,6 +78,11 @@ export const texts = { close: 'Закрыть', /** Сброс черновика формы создания — сам черновик переживает закрытие окна. */ resetDraft: 'Очистить', + /** Предупреждение при выходе из формы редактирования с несохранёнными правками. */ + discardTitle: 'Закрыть без сохранения?', + discardText: 'Внесённые изменения не сохранятся.', + discardConfirm: 'Закрыть без сохранения', + discardKeepEditing: 'Продолжить редактирование', loading: 'Загрузка...', yes: 'Да', no: 'Нет',