From 515c3d73b8e60295653fbd5f0390d8e212e0a092 Mon Sep 17 00:00:00 2001 From: Boris Kovalskii <36034823+JustYay@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:36:31 +1000 Subject: [PATCH] fix(admin): harden legal pages editor state handling --- src/api/adminLegalPages.ts | 16 ++-- src/locales/en.json | 1 + src/locales/ru.json | 1 + src/pages/AdminLegalPages.tsx | 169 ++++++++++++++++++++++++++++------ 4 files changed, 154 insertions(+), 33 deletions(-) diff --git a/src/api/adminLegalPages.ts b/src/api/adminLegalPages.ts index 855862a..d6b3f7e 100644 --- a/src/api/adminLegalPages.ts +++ b/src/api/adminLegalPages.ts @@ -1,6 +1,7 @@ import apiClient from './client'; +import type { InfoPageDisplayMode } from './infoPages'; -export type LegalDisplayMode = 'bot' | 'web' | 'both'; +export type LegalDisplayMode = InfoPageDisplayMode; export interface LegalDocumentItem { language: string; @@ -26,7 +27,7 @@ export interface RulesItem { updated_at: string | null; } -export interface RulesResponse { +export interface AdminRulesResponse { display_mode: LegalDisplayMode; display_mode_env_locked: boolean; items: RulesItem[]; @@ -110,13 +111,16 @@ export const adminLegalPagesApi = { return response.data; }, - getRules: async (): Promise => { - const response = await apiClient.get('/cabinet/admin/legal-pages/rules'); + getRules: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/legal-pages/rules'); return response.data; }, - updateRules: async (data: RulesUpdateRequest): Promise => { - const response = await apiClient.put('/cabinet/admin/legal-pages/rules', data); + updateRules: async (data: RulesUpdateRequest): Promise => { + const response = await apiClient.put( + '/cabinet/admin/legal-pages/rules', + data, + ); return response.data; }, diff --git a/src/locales/en.json b/src/locales/en.json index 5d0718f..9eb484b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4158,6 +4158,7 @@ "save": "Save", "saving": "Saving...", "saveError": "Failed to save. Please try again.", + "unsavedWarning": "Unsaved changes will be lost. Continue?", "faqPages": "Questions and answers", "addQuestion": "Add question", "newQuestion": "New question", diff --git a/src/locales/ru.json b/src/locales/ru.json index 116723a..6a7db49 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -4703,6 +4703,7 @@ "save": "Сохранить", "saving": "Сохранение...", "saveError": "Не удалось сохранить. Попробуйте ещё раз.", + "unsavedWarning": "Несохранённые изменения будут потеряны. Продолжить?", "faqPages": "Вопросы и ответы", "addQuestion": "Добавить вопрос", "newQuestion": "Новый вопрос", diff --git a/src/pages/AdminLegalPages.tsx b/src/pages/AdminLegalPages.tsx index 8424f68..f6b16de 100644 --- a/src/pages/AdminLegalPages.tsx +++ b/src/pages/AdminLegalPages.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -99,7 +99,13 @@ function LanguageTabs({ ); } -function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) { +function DocumentEditor({ + kind, + onDirtyChange, +}: { + kind: 'privacy-policy' | 'public-offer'; + onDirtyChange: (dirty: boolean) => void; +}) { const { t } = useTranslation(); const queryClient = useQueryClient(); const haptic = useHapticFeedback(); @@ -110,17 +116,18 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) { const [populated, setPopulated] = useState(false); const [saveError, setSaveError] = useState(null); - const { data, isLoading } = useQuery({ + const { data, isLoading, isFetching } = useQuery({ queryKey: ['admin', 'legal-pages', kind], queryFn: () => kind === 'privacy-policy' ? adminLegalPagesApi.getPrivacyPolicy() : adminLegalPagesApi.getPublicOffer(), staleTime: 0, + gcTime: 0, }); useEffect(() => { - if (!data || populated) return; + if (!data || isFetching || populated) return; setDisplayMode(data.display_mode); const nextContents: Record = {}; const nextEnabled: Record = {}; @@ -134,7 +141,21 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) { setActiveLang(data.items[0].language); } setPopulated(true); - }, [data, populated]); + }, [data, isFetching, populated]); + + const isDirty = + populated && + !!data && + (displayMode !== data.display_mode || + data.items.some( + (item) => + (contents[item.language] ?? '') !== item.content || + (enabled[item.language] ?? false) !== item.is_enabled, + )); + + useEffect(() => { + onDirtyChange(isDirty); + }, [isDirty, onDirtyChange]); const saveMutation = useMutation({ mutationFn: () => { @@ -187,7 +208,10 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {