Предупреждение при выходе из формы редактирования с несохранёнными правками
Tests / frontend-test (pull_request) Successful in 1m31s
Tests / backend-test (pull_request) Successful in 1m35s

FormModal получил isDirty: если в форме редактирования существующей
записи что-то изменили, крестик, «Отмена» и клик мимо окна сначала
показывают диалог «Закрыть без сохранения?». Подключено к редактированию
типа заявки, маршрута, шага маршрута и согласующего в заявке. Формы
создания не затронуты — их черновик и так сохраняется.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-16 10:02:40 +05:00
parent b94e6e60f5
commit 872686e855
7 changed files with 84 additions and 33 deletions
@@ -74,6 +74,11 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
const excludedSet = new Set(excludedEmails.map(e => 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<IManageRequestsApproverModalP
};
return (
<FormModal title={title} error={submit.error} isSaving={submit.isBusy} onSubmit={handleSubmit} onClose={onClose}>
<FormModal title={title} error={submit.error} isSaving={submit.isBusy} onSubmit={handleSubmit} onClose={onClose} isDirty={isDirty}>
<FormGrid>
<Field label={texts.manageRequests.approverLabel} required full>
<SearchableSelect
@@ -61,6 +61,18 @@ const EMPTY_FORM: IFormState = {
/** Черновик формы создания — переживает закрытие модалки, см. useFormDraft. */
const CREATE_DRAFT_STORAGE_KEY = 'approvalAdmin.draft.requestType';
function toFormState(item: RequestType): IFormState {
return {
name: item.name,
pageUrl: item.pageUrl,
section: item.section,
siteUrl: item.siteUrl ?? '',
listId: item.listId ?? '',
slaHours: slaHoursToInput(item.slaHours),
askOverdueReason: boolToTriState(item.askOverdueReason)
};
}
// siteUrl/listId — поля RequestType, зеркалящие nullable JSON-контракт бэкенда
// (см. models/requestTypes.ts) — здесь сразу приводим null к undefined,
// внутренний код (хуки/кэш SharePointListsClient) с null не работает нигде.
@@ -86,6 +98,7 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
const draft = useFormDraft<IFormState>(CREATE_DRAFT_STORAGE_KEY, EMPTY_FORM);
const [editForm, setEditForm] = React.useState<IFormState>(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<IRequestTypesScreenProps> = ({ 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<IRequestTypesScreenProps> = ({ api, sh
onClose={() => setShowForm(false)}
onReset={editing ? undefined : draft.reset}
canReset={draft.isDirty}
isDirty={isEditDirty}
>
<FormGrid>
<Field label={texts.requestTypes.nameLabel} required full>
@@ -116,6 +116,7 @@ export const RouteFormModal: React.FC<IRouteFormModalProps> = ({
onClose={onClose}
onReset={editing ? undefined : draft.reset}
canReset={draft.isDirty}
isDirty={Boolean(editing) && draft.isDirty}
>
<FormGrid>
<Field label={texts.routes.requestTypeLabel} required full>
@@ -423,6 +423,7 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
onClose={onClose}
onReset={editing ? undefined : draft.reset}
canReset={draft.isDirty}
isDirty={Boolean(editing) && draft.isDirty}
>
<FormGrid>
<Field label={texts.routeSteps.orderNoLabel} required>